1. Интерфейс Serializable
Помните примеры из прошлой лекции? Классы Java, которые мы хотим сериализовать, должны реализовывать специальный интерфейс — java.io.Serializable. Это так называемый маркерный интерфейс: он не содержит ни одного метода, а просто «помечает» класс как подходящий для сериализации. Если класс реализует этот интерфейс, JVM разрешает сериализовать его объекты стандартными средствами.
Сериализовать всё подряд не желательно, потому что не все объекты можно или нужно сохранять в байты. Некоторые объекты зависят от состояния операционной системы, открытых файлов или соединений с сетью. Поэтому Java требует явно пометить класс как сериализуемый.
Маркерный интерфейс — это как наклейка «разрешено к упаковке» на коробке. Если такой наклейки нет — упаковщик (JVM) отказывается работать.
Пример: объявление сериализуемого класса
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
// Конструктор, геттеры и сеттеры
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Для красоты: метод toString()
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
Обратите внимание:
- Мы просто добавили implements Serializable к объявлению класса.
- Не нужно реализовывать никакие методы (интерфейс пустой).
- Все стандартные классы Java, которые можно сериализовать (например, ArrayList, HashMap, String), уже реализуют Serializable.
2. Как сделать свой класс сериализуемым
Правило №1: Просто добавьте implements Serializable
Это всё, что требуется для самого класса. Но есть нюансы!
Важно: сериализуемыми должны быть и все вложенные объекты.
Если у вашего класса есть поля-ссылки на другие объекты, они тоже должны быть сериализуемыми. Например:
public class Profile implements Serializable {
private User user; // User должен быть сериализуемым!
private int level;
}
Если хотя бы одно из полей несериализуемо, при попытке сериализации возникнет исключение.
3. Пример сериализации и десериализации
Давайте посмотрим, как сериализовать и десериализовать объект в файл. Для этого используются классы ObjectOutputStream и ObjectInputStream.
Пример: сериализация объекта User в файл
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
User user = new User("Alice", 30);
// Сохраняем объект в файл
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Объект успешно сериализован в файл user.ser");
} catch (IOException e) {
System.out.println("Ошибка сериализации: " + e.getMessage());
}
}
}
Что здесь происходит?
- Создаём объект User.
- Открываем поток ObjectOutputStream, который пишет в файл "user.ser".
- Вызываем writeObject(user). В этот момент JVM превращает объект в поток байтов и сохраняет его в файл.
Пример: десериализация объекта из файла
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
// Читаем объект из файла
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println("Объект успешно восстановлен: " + user);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Ошибка десериализации: " + e.getMessage());
}
}
}
Что здесь происходит?
- Открываем поток ObjectInputStream, читающий из файла "user.ser".
- Вызываем readObject(). JVM восстанавливает объект из байтов.
- Не забудьте привести результат к нужному типу (User), так как readObject() возвращает Object.
- Может возникнуть ClassNotFoundException, если класс User не найден при десериализации.
Всё вместе: сериализация и десериализация
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
User user = new User("Bob", 22);
// Сериализация
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Сериализация завершена!");
} catch (IOException e) {
System.out.println("Ошибка сериализации: " + e.getMessage());
}
// Десериализация
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User loaded = (User) ois.readObject();
System.out.println("Десериализация завершена! " + loaded);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Ошибка десериализации: " + e.getMessage());
}
}
}
Результат:
Сериализация завершена!
Десериализация завершена! User{name='Bob', age=22}
4. Что происходит «под капотом» при сериализации
Когда вы вызываете writeObject, JVM сначала проверяет, реализует ли класс интерфейс Serializable. Если класс не помечен как сериализуемый, выбрасывается исключение. Затем JVM проходит по всем обычным полям объекта (то есть тем, которые не static и не transient) и записывает их значения в поток байтов. Если среди этих полей встречаются другие объекты, сериализация применяется к ним рекурсивно, но только если они тоже реализуют Serializable.
При десериализации объект создаётся без вызова обычного конструктора, а его поля наполняются сохранёнными значениями — словно «конструктор без конструктора» оживляет объект из байтового потока.
Некоторые поля сериализоваться не будут. Статические поля (static) принадлежат самому классу, а не отдельному объекту, поэтому их значения не сохраняются. Поля, помеченные как transient, тоже пропускаются — это удобно для временных данных, кэша или секретной информации вроде паролей.
Схема процесса сериализации
flowchart TB
A[Объект User в памяти] -- writeObject --> B[ObjectOutputStream]
B -- сохраняет байты --> C[Файл user.ser]
C -- readObject --> D[ObjectInputStream]
D -- восстанавливает --> E[Объект User в памяти]
5. Типичные ошибки при использовании Serializable
Ошибка №1: Поле-ссылка на несериализуемый объект. Если в классе User появится поле типа, например, Thread или Socket, сериализация не сработает. Не все объекты можно сериализовать — помните об этом!
Ошибка №2: Несериализуемые вложенные классы. Если класс User содержит внутренний класс, который не static, то сериализация может не сработать. Лучше использовать static вложенные классы или отдельные классы-файлы.
Ошибка №3: Попытка сериализовать static поле. Статические поля не сериализуются — они принадлежат классу, а не объекту. После десериализации static-поле будет иметь значение, определяемое в классе, а не в сериализованном объекте.
Ошибка №4: Несовпадение версий класса. Если изменить структуру класса после сериализации (например, добавить или удалить поле), а затем попытаться десериализовать старый объект, может возникнуть ошибка InvalidClassException. Для контроля версий используется специальное поле serialVersionUID — о нём подробнее поговорим в следующей лекции.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ