Сериализация объектов в Java: от основ до продвинутых техник
Сериализация — это процесс преобразования состояния объекта в поток байтов для его сохранения (в файл, базу данных) или передачи по сети. Обратный процесс — десериализация — восстанавливает объект из байтового потока. В Java этот механизм встроен в платформу и широко используется в распределенных системах, кэшировании и веб-приложениях. В этой статье мы разберем, как работает сериализация, какие интерфейсы и классы задействованы, и как правильно реализовать кастомную сериализацию.
Основы: интерфейс Serializable
Чтобы объект можно было сериализовать, его класс должен реализовать маркерный интерфейс java.io.Serializable. Этот интерфейс не содержит методов — он лишь сообщает виртуальной машине, что объект разрешено сериализовать.
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
Поле serialVersionUID — уникальный идентификатор версии класса. Если его не объявить явно, JVM вычислит его автоматически, но это может привести к InvalidClassException при десериализации, если класс изменился. Рекомендуется всегда задавать его вручную.
Процесс сериализации и десериализации
Для записи объекта в поток используется ObjectOutputStream, для чтения — ObjectInputStream. Рассмотрим базовый пример:
import java.io.*;
public class SerializationDemo {
public static void main(String[] args) {
User user = new User("Алексей", 30);
// Сериализация
try (FileOutputStream fos = new FileOutputStream("user.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(user);
System.out.println("Объект сериализован: " + user);
} catch (IOException e) {
e.printStackTrace();
}
// Десериализация
try (FileInputStream fis = new FileInputStream("user.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
User restoredUser = (User) ois.readObject();
System.out.println("Объект восстановлен: " + restoredUser);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
При десериализации конструктор объекта не вызывается — состояние восстанавливается напрямую из потока. Это важное отличие от обычного создания объекта через new.
Ключевое слово transient
Иногда нужно исключить некоторые поля из сериализации, например, пароли или временные вычисления. Для этого используется модификатор transient. Поля, помеченные им, при сериализации получают значения по умолчанию (0, null, false).
public class UserWithPassword implements Serializable {
private static final long serialVersionUID = 1L;
private String login;
private transient String password; // не будет сериализовано
public UserWithPassword(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public String toString() {
return "User{login='" + login + "', password='" + password + "'}";
}
}
После десериализации поле password будет null. Это удобно для безопасности, но требует дополнительной логики восстановления.
Кастомная сериализация: методы writeObject и readObject
Если стандартное поведение не устраивает, можно переопределить приватные методы writeObject(ObjectOutputStream oos) и readObject(ObjectInputStream ois). Они вызываются автоматически во время сериализации/десериализации.
import java.io.*;
public class CustomUser implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String hash;
public CustomUser(String name, String hash) {
this.name = name;
this.hash = hash;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // сериализуем обычные поля
oos.writeObject(hash); // пишем transient-поле вручную
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // восстанавливаем обычные поля
this.hash = (String) ois.readObject(); // читаем хэш
}
@Override
public String toString() {
return "CustomUser{name='" + name + "', hash='" + hash + "'}";
}
}
Метод defaultWriteObject() сериализует все не-transient поля, а defaultReadObject() восстанавливает их. Дополнительно мы пишем и читаем transient-поле, что позволяет сохранить его, но с контролем процесса.
Интерфейс Externalizable: полный контроль
Для