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.
- Make only
Aspirantserializable:implements Serializableand an explicitserialVersionUID. Do not changeStudentyet. Write the methodsstatic void saveGroup(Student[] group, String fileName)andstatic Student[] loadGroup(String fileName)that write and read the whole array. Close the streams with try-with-resources. - Save a group of one student and one aspirant. Which exception do you get, and on which object, given that
Aspirantis a subclass ofStudentand is serializable? - Save a group of a single aspirant and read it back. If
Studenthas only a constructor with parameters, reading fails: with which exception, and why was there no error on writing? Add an empty no-arg constructor toStudentand try again. Print the aspirant withReflectionToString.toString()before writing and after reading:
Explain why theAspirant{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}Studentfields are lost whileresearchWorkis restored. - Without making
Studentserializable, add privatewriteObject()andreadObject()methods toAspirantthat save and restore theStudentfields (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. - Now make
Studentitself serializable and removewriteObject()/readObject()fromAspirant. DoesAspirantstill needimplements Serializable? Save and read a group of a student and an aspirant and callgetScholarship()for every element of the array you read: the amounts must match the ones before writing, and the second element must still be anAspirant. - 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?
- Mark the
averageMarkfield astransient, save and read the group again and compare the scholarships. Why must such a field not betransient? 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.
- Make the cart class and every class it refers to (product, cart item, customer) serializable. Declare
serialVersionUIDin each of them. - After a purchase is completed, save the cart to the file
last-cart.ser, overwriting the previous one. - 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 yetinstead of an exception stack trace. - 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. - Check compatibility: add a new field to the product class without changing
serialVersionUIDand read the old file. Then changeserialVersionUIDand 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()
} - Implement a shallow copy in
Orderwithclone(). Print the results ofcopy == order,copy.getItems() == order.getItems()andcopy.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. - Make
clone()deep: the copy must have its own list, its ownItemobjects and its ownCustomer. The method must not declarethrows CloneNotSupportedException. - 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 (ByteArrayOutputStreamandByteArrayInputStream). - For each of the three ways, check that after
copy.getItems().get(0).setPrice(0),copy.getItems().add(...)andcopy.getCustomer().setName(...)the original has not changed. - Declare the
itemsfield asfinal. 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
ObjectOutputStreamcan leave the file incomplete, and the next read fails withEOFExceptionorStreamCorruptedException. - An empty catch block. If you catch
IOExceptionand print nothing, aNotSerializableExceptionwith the name of the offending class goes unnoticed, and the program loadsnull. - Copying the list instead of its elements.
new ArrayList<>(items)anditems.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 returnstruefor 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