Serialization ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-11-03

Serialization. Practical Tasks

A class Horse implements Serializable, but its parent Animal does not. You write a horse named Bucephalus to a file, read it back — and the name field is null. Remove the no-arg constructor from Animal, and writing still succeeds, but readObject() throws InvalidClassException: no valid constructor. Such rules stick much better once you have written the code yourself.

These Java serialization and cloning exercises practice the module «Serialization and Cloning in Java»: writing and reading objects with ObjectOutputStream and ObjectInputStream, transient and serialVersionUID, serializing a subclass of a non-serializable class, saving the shopping cart of the online shop to a file, inspecting a class with reflection, and making a deep copy of an object in three ways. Before you start, read the lessons Serialization and Deserialization in Java and Object Cloning in Java.

Student and Aspirant: Save the Group to a File (Serialization)

Student and Aspirant · part 4 of 4 · ← part 3

This continues the Student and Aspirant project. Take the Student and Aspirant classes from the exercise «Inheritance: Student and Aspirant», the static counter from «A counter of Students» and the ReflectionToString.toString() method from «Universal toString() with Reflection». Now the group has to be saved to a file and loaded back.

  1. Make only Aspirant serializable: implements Serializable and an explicit serialVersionUID. Do not change Student yet. Write the methods static void saveGroup(Student[] group, String fileName) and static Student[] loadGroup(String fileName) that write and read the whole array. Close the streams with try-with-resources.
  2. Save a group of one student and one aspirant. Which exception do you get, and on which object, given that Aspirant is a subclass of Student and is serializable?
  3. Save a group of a single aspirant and read it back. If Student has only a constructor with parameters, reading fails: with which exception, and why was there no error on writing? Add an empty no-arg constructor to Student and try again. Print the aspirant with ReflectionToString.toString() before writing and after reading:
    Aspirant{firstName=John, lastName=Brown, group=PH-21, averageMark=5.0, researchWork=Quantum computing}
    Aspirant{firstName=null, lastName=null, group=null, averageMark=0.0, researchWork=Quantum computing}
    Explain why the Student fields are lost while researchWork is restored.
  4. Without making Student serializable, add private writeObject() and readObject() methods to Aspirant that save and restore the Student fields (add getters and setters if you do not have them yet). Make sure that after reading the second line of the output matches the first one.
  5. Now make Student itself serializable and remove writeObject()/readObject() from Aspirant. Does Aspirant still need implements Serializable? Save and read a group of a student and an aspirant and call getScholarship() for every element of the array you read: the amounts must match the ones before writing, and the second element must still be an Aspirant.
  6. Print the value of the static student counter before writing and after reading. Why did reading two objects not increase it? What does the counter show in a separate program that only reads the file?
  7. Mark the averageMark field as transient, save and read the group again and compare the scholarships. Why must such a field not be transient? Remove the modifier again.

Hint

On reading, a constructor is called only for the first non-serializable ancestor, and it is that constructor that sets the fields of this ancestor. If the ancestor has no accessible no-arg constructor, readObject() throws InvalidClassException: no valid constructor. Constructors of serializable classes are not called at all, so count++ in the constructor does not run. For step 4: private void writeObject(ObjectOutputStream out) calls out.defaultWriteObject() and then writes the parent fields, and private void readObject(ObjectInputStream in) calls in.defaultReadObject() and reads those fields in the same order.

Save the Last Cart with Serialization

Online Store · part 10 of 11 · ← part 10

Continue the online shop project: a customer should see their last purchase even after the program restarts.

  1. Make the cart class and every class it refers to (product, cart item, customer) serializable. Declare serialVersionUID in each of them.
  2. After a purchase is completed, save the cart to the file last-cart.ser, overwriting the previous one.
  3. When the program starts, load the last cart and print its contents and the total. If there is no file yet (the first run), print No purchases yet instead of an exception stack trace.
  4. Mark fields that should not or cannot be saved (for example, a reference to the payment service or a cached total) as transient, and restore them after reading.
  5. Check compatibility: add a new field to the product class without changing serialVersionUID and read the old file. Then change serialVersionUID and note which exception you get when reading the same file.

Where to look for the bug

If writing fails with NotSerializableException, the message names the class that has to become Serializable or whose field has to be marked transient. The whole graph is serialized: one non-serializable object in any cart item is enough to break it. Check that the file exists with Files.exists(path) before opening the stream.

Deep Copy of an Order: clone(), Copy Constructor and Serialization

You are given an order class:

public class Order {
    private final String id;
    private Customer customer;   // Customer: mutable fields name and email
    private List<Item> items;   // Item: mutable fields title and price

    // constructor, getters, toString()
}
  1. Implement a shallow copy in Order with clone(). Print the results of copy == order, copy.getItems() == order.getItems() and copy.getItems().get(0) == order.getItems().get(0), then change the price of the first item in the copy and print the original. Expected results of the comparisons: false, true, true.
  2. Make clone() deep: the copy must have its own list, its own Item objects and its own Customer. The method must not declare throws CloneNotSupportedException.
  3. Write two more ways to make a deep copy: a copy constructor Order(Order other) and a utility method that copies the order through in-memory serialization (ByteArrayOutputStream and ByteArrayInputStream).
  4. For each of the three ways, check that after copy.getItems().get(0).setPrice(0), copy.getItems().add(...) and copy.getCustomer().setName(...) the original has not changed.
  5. Declare the items field as final. Which of the three ways stops compiling, and why?

Check yourself

A copy is deep if no mutable reference is shared: on every level (Order → list → Item, Order → Customer) the == comparison gives false. new ArrayList<>(items) copies only the list itself, not its elements. The id field does not need copying: String is immutable.

What Each Task Practices

Three tasks practice built-in Java serialization (inheritance, transient, compatibility of class versions and what a class looks like to the serialization mechanism), and the fourth practices cloning and deep copying. If a solution does not work, go back to the lesson section from the table: each one has the rule and an example with its output.

Task What it checks What to review
Horse and Animal a parent without Serializable, the constructor on reading, transient, writeObject()/readObject() Deserialization and inheritance, writeObject() and readObject(): the rules
The last cart the object graph, NotSerializableException, serialVersionUID after a class change The object graph, Compatible and incompatible class changes
Class metadata Reflection API, the static and transient modifiers, Serializable and Cloneable in superclasses The transient keyword in Java
Deep copy of an order shallow and deep copy, clone() and final fields, a copy through serialization Deep copy with clone(), clone() gotchas, Deep copy via serialization

What Most Often Goes Wrong in Solutions

Most mistakes in these tasks do not show up on the first run: the program writes the file without any exception, and the problem appears only on reading, after the class changes or when the copy is modified. So test every solution with the full cycle «write — change — read», not with a single write.

  • Expecting the constructor to run on reading. The constructor of a serializable class is not called, so its logic (default values, checks) does not run after readObject().
  • Streams without try-with-resources. An unclosed ObjectOutputStream can leave the file incomplete, and the next read fails with EOFException or StreamCorruptedException.
  • An empty catch block. If you catch IOException and print nothing, a NotSerializableException with the name of the offending class goes unnoticed, and the program loads null.
  • Copying the list instead of its elements. new ArrayList<>(items) and items.clone() create a new list that holds the same objects — it is still a shallow copy.
  • Checking a copy with equals(). equals() compares contents and returns true for both a deep and a shallow copy. Only == on nested objects, and changing the copy and then printing the original, show whether the copy is independent.

Check your code

Before you compare your solution with a reference one, go through the code style checklist: the file name is a constant, streams are closed with try-with-resources, every serializable class declares an explicit serialVersionUID, and no exception is swallowed by an empty catch.

Frequently Asked Questions

In what order should I solve the serialization and cloning exercises?

Start with the Horse and Animal task: it covers all the basic moves of writing and reading an object. Then solve the cart task — it applies the same moves to the object graph of the project. The reflection and deep copy tasks are independent, but one of the copy methods relies on serialization, so the deep copy task is best left for last.

Can I save objects as JSON instead of using ObjectOutputStream?

Not in these tasks: they practice built-in Java serialization (Serializable, transient, serialVersionUID), and JSON libraries such as Jackson and Gson follow their own rules. In real projects JSON is indeed the more common choice for storing and exchanging data. How the formats compare is covered in the section Serialization vs marshalling.

Why is there a reflection task in a serialization module?

Standard serialization itself reads and writes fields through reflection, bypassing getters and access modifiers. When you print the fields of a class with their modifiers, you see exactly what goes into the stream: everything except static and transient fields. For example, the element array of ArrayList is declared transient, and the collection writes its elements itself.

How do I make sure a copy of an object is really deep?

Compare every mutable reference of the original and the copy with == on all levels: each comparison must give false. Then change a nested object in the copy and print the original — it must stay the same. An example of such a check is in the section The clone() method and the Cloneable interface.

Comments

Please log in or register to have a possibility to add comment.