1. Введение: сериализация объектов с полями-ссылками
В реальных приложениях редко встречаются полностью «плоские» классы. Обычно объект содержит другие объекты, а те, в свою очередь, могут содержать ещё что-то. Это называется композицией (или вложенностью) объектов. Например:
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // Вложенный объект!
}
Когда мы сериализуем такой объект, возникает вопрос: а что делать с полем address? Должна ли Java сериализовать его вместе с Person? А если у Address внутри ещё какой-то объект? К счастью (или к сожалению — зависит от ситуации), Java по умолчанию сериализует все вложенные объекты рекурсивно, если они тоже реализуют интерфейс Serializable.
Сериализация в Java — это всегда глубокая сериализация (deep serialization). Это значит, что сериализуется не только сам объект, но и все объекты, на которые он ссылается через свои (не transient) поля, и так далее — до самого «дна».
Визуализация процесса
Словом, если решили сериализовать Person, то Java сериализует и Address, и всё, что внутри Address, и так далее.
2. Требования к вложенным объектам: Serializable обязателен!
Вы уже, вероятно, обратили внимание на важный нюанс: все вложенные объекты, которые сериализуются, тоже должны реализовывать интерфейс Serializable.
Если хотя бы одно поле-ссылка указывает на объект, который не реализует Serializable, попытка сериализации закончится исключением java.io.NotSerializableException.
Давайте рассмотрим примеры глубокой сериализации.
Пример: всё хорошо
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Оба класса реализуют Serializable. Всё работает, сериализация проходит успешно.
Пример: ошибка!
public class Address { // НЕ реализует Serializable!
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Как видим, здесь Address не реализует интерфейс Serializable. Так что при сериализации Person мы получим исключение NotSerializableException.
3. Пример: сериализация и десериализация с вложенным объектом
Давайте посмотрим, как это выглядит на практике. На прошлом уровне мы работали с приложением «Менеджер контактов». Давайте теперь добавим в него адрес каждого пользователя.
import java.io.*;
class Address implements Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Serializable {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Person p = new Person("Иван", 30, new Address("Прага", "Славинска, 1"));
// Сериализация
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Десериализация
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name + ", " + restored.age + ", " +
restored.address.city + ", " + restored.address.street);
}
}
Результат:
Иван, 30, Прага, Славинска, 1
Всё работает: вложенный объект Address сериализован и восстановлен вместе с Person.
4. Что если вложенный объект не сериализуем?
Если попытаться сериализовать объект, у которого хотя бы одно поле-ссылка указывает на объект, не реализующий Serializable, Java выбросит исключение уже при попытке сериализации.
Демонстрация ошибки
class Address { // не Serializable!
String city;
String street;
}
class Person implements Serializable {
String name;
Address address;
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.name = "Петя";
p.address = new Address();
p.address.city = "Дерри";
p.address.street = "Вязов";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- БУДЕТ ИСКЛЮЧЕНИЕ!
out.close();
}
}
Ошибка:
java.io.NotSerializableException: Address
5. Transient для вложенных объектов
Что делать, если у вас есть поле-ссылка на объект, который не должен сериализоваться (например, кэш, соединение с базой данных, временный объект)? В таком случае объявите поле как transient. Тогда Java просто пропустит это поле при сериализации.
Пример с transient
class Address { // не Serializable
String city;
String street;
}
class Person implements Serializable {
String name;
transient Address address; // transient!
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Address addr = new Address();
addr.city = "Лос-Сантос";
addr.street = "Малхолланд Драйв";
Person p = new Person("Саша", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Десериализация
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "Саша"
System.out.println(restored.address); // null!
}
}
Результат:
Поле address после десериализации равно null, потому что оно transient.
6. Глубокая вложенность и рекурсия
Сериализация работает рекурсивно: если у Person есть поле Address, а у Address есть поле CityInfo, и так далее, сериализатор будет «нырять» всё глубже, пока не встретит что-то несериализуемое или пока не закончится память (шутка, но только наполовину).
Важно: циклические ссылки
Java-сериализатор умеет работать с циклическими ссылками. Если, например, у объекта есть ссылка на другой объект, который в свою очередь ссылается обратно, сериализатор не зациклится, а аккуратно сохранит структуру.
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
Если создать объекты A и B, которые ссылаются друг на друга, сериализация не вызовет StackOverflowError — Java помнит уже сериализованные объекты.
7. Пример: сериализация объекта с вложенным списком
Часто внутри объекта есть коллекции других объектов. Например, у пользователя может быть список друзей:
import java.io.*;
import java.util.*;
class Person implements Serializable {
String name;
List<Person> friends;
Person(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
}
public class FriendsSerialization {
public static void main(String[] args) throws Exception {
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friends.add(bob);
// Сериализация
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// Десериализация
ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // Alice
System.out.println(restored.friends.get(0).name); // Bob
}
}
Важно: Коллекции стандартной библиотеки Java (ArrayList, HashMap и др.) реализуют Serializable, так что всё работает «из коробки».
8. Типичные ошибки при сериализации вложенных объектов
Ошибка №1: Один из вложенных объектов не реализует Serializable. Вы забыли добавить implements Serializable в один из вложенных классов. Итог — NotSerializableException при первой попытке сериализации. Проверяйте, что все классы в вашей цепочке сериализации поддерживают этот интерфейс.
Ошибка №2: Несериализуемое поле не объявлено transient. Если у вас есть поле, которое не сериализуется (например, поток, соединение с базой, что-то временное), но вы не объявили его как transient, сериализация упадёт с ошибкой. Не забывайте про transient!
Ошибка №3: Несовпадение serialVersionUID во вложенных классах. Если вы явно объявляете serialVersionUID во вложенных классах и меняете их структуру, не забывайте обновлять этот идентификатор — иначе возможны ошибки при десериализации.
Ошибка №4: Мутирующие (изменяемые) вложенные объекты. Если вы сериализуете коллекцию или объект, который потом меняется (например, список друзей), после десериализации это уже будет «снимок» на момент сериализации. Новые изменения в оригинале никак не отразятся в десериализованном объекте.
Ошибка №5: Сериализация огромных графов объектов. Если у вас очень сложная структура с множеством вложенных объектов, сериализация может занять много времени и памяти. Иногда лучше сериализовать только ключевые данные, а не всю структуру целиком.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ