Сериализация ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-11-04

Сериализация и клонирование объектов в Java. Практические Задания

Класс Aspirant реализует Serializable, его родитель Student — нет. Аспиранта записали в файл, прочитали обратно — и имя, фамилия, группа и средний балл стали null и 0.0, уцелела только тема научной работы. А если у Student нет конструктора без параметров, запись пройдёт успешно, но readObject() бросит InvalidClassException: no valid constructor. Такие вещи надёжнее понять, написав код, чем прочитав о них.

На этой странице — задачи по сериализации и клонированию объектов в Java: запись и чтение объектов через ObjectOutputStream и ObjectInputStream, transient и serialVersionUID, сохранение группы из проекта «Студент и Аспирант», где родитель не сериализуем, сохранение корзины интернет-магазина в файл и глубокая копия объекта тремя способами. Перед решением прочитайте уроки модуля: Сериализация и десериализация объектов в Java и Клонирование объектов в Java.

Студент и Аспирант: сохранение группы в файл (сериализация)

Студент и Аспирант · часть 4 из 4 · ← часть 3

Продолжение проекта «Студент и Аспирант». Возьмите классы Student и Aspirant из задачи «Наследование Student, Aspirant», статический счётчик из задачи «Статический счетчик студентов в группе» и метод ReflectionToString.toString() из задачи «Универсальный toString() через рефлексию». Теперь группу нужно сохранять в файл и загружать обратно.

  1. Сделайте сериализуемым только класс Aspirant: implements Serializable и явный serialVersionUID. Класс Student пока не меняйте. Напишите методы static void saveGroup(Student[] group, String fileName) и static Student[] loadGroup(String fileName), которые записывают и читают массив целиком. Потоки закрывайте через try-with-resources.
  2. Сохраните группу из одного студента и одного аспиранта. Какое исключение вы получите и на каком объекте, если Aspirant — наследник Student и сериализуется?
  3. Сохраните группу из одного аспиранта и прочитайте её. Если у Student есть только конструктор с параметрами, чтение упадёт: с каким исключением и почему при записи ошибки не было? Добавьте в Student пустой конструктор без параметров и повторите. Выведите аспиранта через ReflectionToString.toString() до записи и после чтения:
    Aspirant{firstName=Иван, lastName=Петров, group=ФИ-21, averageMark=5.0, researchWork=Квантовые вычисления}
    Aspirant{firstName=null, lastName=null, group=null, averageMark=0.0, researchWork=Квантовые вычисления}
    Объясните, почему поля Student пропали, а researchWork восстановилось.
  4. Не делая Student сериализуемым, добавьте в Aspirant приватные методы writeObject() и readObject(), которые сохраняют и восстанавливают поля Student (если геттеров и сеттеров ещё нет, добавьте их). Убедитесь, что после чтения первая строка вывода совпадает со второй.
  5. Теперь сделайте сериализуемым сам Student и удалите writeObject()/readObject() из Aspirant. Нужно ли оставлять implements Serializable у Aspirant? Сохраните и прочитайте группу из студента и аспиранта и вызовите getScholarship() для каждого элемента прочитанного массива: суммы должны совпасть с суммами до записи, а второй элемент должен остаться Aspirant.
  6. Выведите значение статического счётчика студентов до записи и после чтения. Почему чтение двух объектов его не увеличило? Что покажет счётчик в отдельной программе, которая только читает файл?
  7. Пометьте поле averageMark модификатором transient, снова сохраните и прочитайте группу и сравните стипендии. Почему такое поле нельзя делать transient? Верните модификатор обратно.

Подсказка

При чтении конструктор вызывается только у первого несериализуемого предка, и именно он заполняет поля этого предка. Если у такого предка нет доступного конструктора без параметров, readObject() бросает InvalidClassException: no valid constructor. Конструкторы сериализуемых классов не вызываются вовсе, поэтому count++ из конструктора не сработает. Для шага 4: private void writeObject(ObjectOutputStream out) вызывает out.defaultWriteObject() и дописывает поля родителя, а private void readObject(ObjectInputStream in) вызывает in.defaultReadObject() и читает эти поля в том же порядке.

Сохранение последней корзины (сериализация)

Интернет-магазин · часть 11 из 13 · ← часть 10 · часть 12 →

Продолжите проект интернет-магазина: покупатель должен видеть свою последнюю покупку и после перезапуска программы.

  1. Сделайте сериализуемыми класс корзины и все классы, на которые она ссылается (товар, позиция корзины, покупатель). Объявите в каждом из них serialVersionUID.
  2. После оформления покупки сохраняйте корзину в файл last-cart.ser, перезаписывая предыдущую.
  3. При запуске программы загружайте последнюю корзину и выводите её содержимое и итоговую сумму. Если файла нет (первый запуск), выведите «Покупок ещё не было» вместо трассировки исключения.
  4. Поля, которые не нужно или нельзя сохранять (например, ссылку на сервис оплаты или закешированную итоговую сумму), пометьте transient и восстановите их после чтения.
  5. Проверьте совместимость: добавьте в класс товара новое поле, не меняя serialVersionUID, и прочитайте старый файл. Затем измените serialVersionUID и запишите, какое исключение получили при чтении того же файла.

Где искать ошибку

Если запись падает с NotSerializableException, в сообщении указано имя класса, который нужно сделать Serializable или поле которого пометить transient. Сериализуется весь граф: достаточно одного несериализуемого объекта в любой позиции корзины. Наличие файла удобно проверить через Files.exists(path) до открытия потока.

Глубокая копия заказа: clone(), конструктор копирования и сериализация

Дан класс заказа:

public class Order {
    private final String id;
    private Customer customer;   // Customer: изменяемые поля name и email
    private List<Item> items;   // Item: изменяемые поля title и price

    // конструктор, геттеры, toString()
}
  1. Реализуйте в Order поверхностную копию через clone(). Выведите результаты выражений copy == order, copy.getItems() == order.getItems() и copy.getItems().get(0) == order.getItems().get(0), затем измените цену первого товара в копии и выведите оригинал. Ожидаемые значения сравнений: false, true, true.
  2. Сделайте clone() глубоким: у копии должны быть свой список, свои объекты Item и свой Customer. Метод не должен объявлять throws CloneNotSupportedException.
  3. Напишите ещё два способа глубокого копирования: конструктор копирования Order(Order other) и утилиту, которая копирует заказ через сериализацию в памяти (ByteArrayOutputStream и ByteArrayInputStream).
  4. Для каждого из трёх способов проверьте, что после copy.getItems().get(0).setPrice(0), copy.getItems().add(...) и copy.getCustomer().setName(...) оригинал не изменился.
  5. Объявите поле items как final. Какой из трёх способов перестанет компилироваться и почему?

Проверьте себя

Копия глубокая, если ни одна изменяемая ссылка не общая: на каждом уровне (Order → список → Item, Order → Customer) сравнение через == даёт false. new ArrayList<>(items) копирует только сам список, но не его элементы. Поле id копировать не нужно: String неизменяем.

Что закрепляет каждая задача

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

Задача Что проверяет Что повторить
Студент и Аспирант родитель без Serializable, конструктор и static-счётчик при чтении, transient, writeObject()/readObject() Десериализация и наследование
Последняя корзина граф объектов, NotSerializableException, serialVersionUID при изменении класса serialVersionUID и версии класса
Глубокая копия заказа поверхностная и глубокая копия, clone() и final-поля, копия через сериализацию Глубокое копирование через clone(), подводные камни clone()

На чём чаще всего ошибаются в решениях

Большинство ошибок в этих задачах не видно на первом запуске: программа записывает файл без исключений, а проблема проявляется только при чтении, после изменения класса или при правке копии. Поэтому проверяйте решение полным циклом «записать — изменить — прочитать», а не одной записью.

  • Ожидание, что при чтении вызовется конструктор. Конструктор сериализуемого класса не вызывается, поэтому логика из него (значения по умолчанию, проверки) после readObject() не выполнится.
  • Потоки без try-with-resources. Незакрытый ObjectOutputStream может оставить файл недописанным, и следующее чтение упадёт с EOFException или StreamCorruptedException.
  • Пустой catch. Если перехватить IOException и ничего не вывести, NotSerializableException с именем проблемного класса пройдёт незамеченным, а программа загрузит null.
  • Копия списка вместо копии элементов. new ArrayList<>(items) и items.clone() создают новый список, но с теми же объектами внутри — это всё ещё поверхностная копия.
  • Проверка копии через equals(). equals() сравнивает содержимое и для глубокой, и для поверхностной копии вернёт true. Независимость копии показывает только == для вложенных объектов и изменение копии с последующей печатью оригинала.

Проверь свой код

Перед тем как сверяться с разбором, пройдитесь по рекомендациям по оформлению кода: имя файла вынесено в константу, потоки закрываются через try-with-resources, у каждого сериализуемого класса есть явный serialVersionUID, а исключения не глотаются пустым catch.

Часто задаваемые вопросы

В каком порядке решать задачи по сериализации и клонированию?

Начните с задачи про Student и Aspirant: в ней все базовые приёмы записи и чтения объекта, и она продолжает учебный проект из модулей об ООП, static и рефлексии. Затем решайте задачу про корзину — она применяет те же приёмы к графу объектов проекта. Задачу о глубокой копии удобнее решать последней: один из способов копирования опирается на сериализацию.

Можно ли сохранять объекты в JSON вместо ObjectOutputStream?

В этих задачах — нет: они проверяют встроенную сериализацию Java (Serializable, transient, serialVersionUID), а у JSON-библиотек вроде Jackson и Gson свои правила. В реальных проектах для хранения и обмена данными JSON действительно выбирают чаще. Как сравниваются форматы — в разделе Сериализация и маршалинг.

Как убедиться, что копия объекта действительно глубокая?

Сравните через == каждую изменяемую ссылку оригинала и копии на всех уровнях: везде должно получиться false. Затем измените вложенный объект в копии и выведите оригинал — он не должен измениться. Пример такой проверки — в разделе Метод clone() и интерфейс Cloneable.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.