Сериализация и клонирование объектов в 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() через рефлексию». Теперь группу нужно сохранять в файл и загружать обратно.
- Сделайте сериализуемым только класс
Aspirant:implements Serializableи явныйserialVersionUID. КлассStudentпока не меняйте. Напишите методыstatic void saveGroup(Student[] group, String fileName)иstatic Student[] loadGroup(String fileName), которые записывают и читают массив целиком. Потоки закрывайте через try-with-resources. - Сохраните группу из одного студента и одного аспиранта. Какое исключение вы получите и на каком объекте, если
Aspirant— наследникStudentи сериализуется? - Сохраните группу из одного аспиранта и прочитайте её. Если у
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восстановилось. - Не делая
Studentсериализуемым, добавьте вAspirantприватные методыwriteObject()иreadObject(), которые сохраняют и восстанавливают поляStudent(если геттеров и сеттеров ещё нет, добавьте их). Убедитесь, что после чтения первая строка вывода совпадает со второй. - Теперь сделайте сериализуемым сам
Studentи удалитеwriteObject()/readObject()изAspirant. Нужно ли оставлятьimplements SerializableуAspirant? Сохраните и прочитайте группу из студента и аспиранта и вызовитеgetScholarship()для каждого элемента прочитанного массива: суммы должны совпасть с суммами до записи, а второй элемент должен остатьсяAspirant. - Выведите значение статического счётчика студентов до записи и после чтения. Почему чтение двух объектов его не увеличило? Что покажет счётчик в отдельной программе, которая только читает файл?
- Пометьте поле
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 →
Продолжите проект интернет-магазина: покупатель должен видеть свою последнюю покупку и после перезапуска программы.
- Сделайте сериализуемыми класс корзины и все классы, на которые она ссылается (товар, позиция корзины, покупатель). Объявите в каждом из них
serialVersionUID. - После оформления покупки сохраняйте корзину в файл
last-cart.ser, перезаписывая предыдущую. - При запуске программы загружайте последнюю корзину и выводите её содержимое и итоговую сумму. Если файла нет (первый запуск), выведите «Покупок ещё не было» вместо трассировки исключения.
- Поля, которые не нужно или нельзя сохранять (например, ссылку на сервис оплаты или закешированную итоговую сумму), пометьте
transientи восстановите их после чтения. - Проверьте совместимость: добавьте в класс товара новое поле, не меняя
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()
} - Реализуйте в
Orderповерхностную копию черезclone(). Выведите результаты выраженийcopy == order,copy.getItems() == order.getItems()иcopy.getItems().get(0) == order.getItems().get(0), затем измените цену первого товара в копии и выведите оригинал. Ожидаемые значения сравнений:false,true,true. - Сделайте
clone()глубоким: у копии должны быть свой список, свои объектыItemи свойCustomer. Метод не должен объявлятьthrows CloneNotSupportedException. - Напишите ещё два способа глубокого копирования: конструктор копирования
Order(Order other)и утилиту, которая копирует заказ через сериализацию в памяти (ByteArrayOutputStreamиByteArrayInputStream). - Для каждого из трёх способов проверьте, что после
copy.getItems().get(0).setPrice(0),copy.getItems().add(...)иcopy.getCustomer().setName(...)оригинал не изменился. - Объявите поле
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.
Комментарии