CodeGym /Courses /JAVA 25 SELF /Controlling the serialization process: writeObject, readO...

Controlling the serialization process: writeObject, readObject

JAVA 25 SELF
Level 43 , Lesson 0
Available

1. Introduction

Automatic serialization is like autopilot in an airplane: it works great as long as everything goes according to plan. But as soon as special conditions appear, it becomes clear that the simple mechanism is no longer enough. Imagine you need to persist an object, but not all of its fields: some data are temporary, and some are too sensitive to write to a file. Or the other way around — when saving, you need to add something of your own: for example, a version or a checksum. Sometimes, before writing or loading the data, you need to perform a check or a transformation. And sometimes the task is even harder: ensure compatibility with previous versions of the class if its structure has changed over time.

In such situations it becomes clear: standard serialization alone is not enough. You need to take control.

Special serialization methods: writeObject and readObject

Java provides two special methods that let you fully control the serialization and deserialization of an object:

private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

Important!

  • The methods must be private (not public, not protected, not package-private).
  • The signatures must match those shown above.
  • If these methods are declared in your class, they will be invoked instead of the default serialization/deserialization.

How does it work?

When you call ObjectOutputStream.writeObject(obj), the JVM first looks in the class of obj for a method private void writeObject(ObjectOutputStream). If it exists, that is what gets called. Similarly, during deserialization a private void readObject(ObjectInputStream) is invoked.

If the methods are not declared, default serialization is used.

How writeObject and readObject are structured

Method signatures

private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

Inside these methods you must call:

  • out.defaultWriteObject(); — to serialize the standard (non-transient) fields of the superclass and the current class.
  • in.defaultReadObject(); — to deserialize the standard fields.

If you don’t call these methods, the standard fields will not be serialized — and upon deserialization the object will be “empty.” It’s like forgetting to put your passport in your suitcase: technically you arrived, but you won’t be able to prove who you are.

2. Example: adding a checksum during serialization

Let’s look at a practical example. Suppose we have a user class, and when serializing we want to add a checksum to the object to verify data integrity during deserialization.

import java.io.*;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    // transient field — do not serialize it
    private transient int checksum;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
        this.checksum = calculateChecksum();
    }

    private int calculateChecksum() {
        return (name != null ? name.hashCode() : 0) + age;
    }

    // Custom serialization
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // Save standard fields
        int sum = calculateChecksum();
        out.writeInt(sum); // Write the checksum
        System.out.println("[LOG] Serializing User: checksum=" + sum);
    }

    // Custom deserialization
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // Restore standard fields
        int sum = in.readInt(); // Read the checksum
        int actual = calculateChecksum();
        System.out.println("[LOG] Deserializing User: checksum=" + sum + ", actual=" + actual);
        if (sum != actual) {
            throw new IOException("Data is corrupted! Checksum does not match.");
        }
        this.checksum = actual;
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
    }
}

Usage example:

// Save the object
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
    out.writeObject(user);
}

// Load the object
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
    User loaded = (User) in.readObject();
    System.out.println("Restored object: " + loaded);
}

What happens?

  • During serialization, writeObject is called; standard fields and the checksum are saved.
  • During deserialization, readObject is called; fields are restored and the checksum is verified.
  • You’ll see a log in the console, and if something is wrong, an exception will be thrown.

3. Excluding sensitive data from serialization

Sometimes you need certain fields not to be serialized (for example, passwords). You can use the transient keyword (more on that in the next lecture), or you can manually skip serializing a field if you implement writeObject.

Example:

public class Account implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password; // transient — not serialized

    // But you can also do this:
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        // Do not write password!
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // password remains null
    }
}

Note:
If you want to serialize only part of the object, simply don’t write the extra fields to the stream.

4. Invoking defaultWriteObject and defaultReadObject

Inside your writeObject and readObject methods, you almost always need to call defaultWriteObject() and defaultReadObject(). It’s like hitting “save draft” before adding your own notes.

These methods perform the default serialization of all non-transient, non-static fields of the current class and its superclass. If you don’t call them, these fields won’t be serialized and will be empty after deserialization.

Example of incorrect behavior:

private void writeObject(ObjectOutputStream out) throws IOException {
    // out.defaultWriteObject(); // forgot to call it!
    out.writeInt(123); // your custom data
}

In this case the standard fields simply won’t be saved!

5. Practice: logging the serialization process

Let’s add logging to our user class to see when serialization and deserialization happen.

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("[LOG] Serializing Person: " + name + ", age " + age);
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        System.out.println("[LOG] Deserializing Person: " + name + ", age " + age);
    }
}

Usage:

Person p = new Person("Bob", 30);
// Save to file
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
    out.writeObject(p);
}
// Load from file
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
    Person loaded = (Person) in.readObject();
}

Result:
You will see messages in the console indicating that the object is being serialized and deserialized.

6. Common mistakes when using writeObject/readObject

Error #1: defaultWriteObject/defaultReadObject not called. If you forget to call these methods, the standard fields will not be serialized, and after deserialization the object will be empty or incorrect.

Error #2: Incorrect method signature. The methods must be strictly private void writeObject(ObjectOutputStream) and private void readObject(ObjectInputStream). If you make them public/protected or change the parameters, they will not be invoked automatically.

Error #3: Exception in the method. If an exception occurs in writeObject or readObject, serialization or deserialization will be interrupted, and the object will not be saved/loaded correctly.

Error #4: Forgotten superclass serialization/deserialization. If your class extends another serializable class, be sure to call defaultWriteObject/defaultReadObject, otherwise the superclass fields will not be saved.

Error #5: Serializing sensitive data. If you forget to exclude passwords or other private data, they will end up in the serialized file. Use transient or skip serializing them manually.

1
Task
JAVA 25 SELF, level 43, lesson 0
Locked
Magic Cat Entity: Saving and Awakening with Logging
Magic Cat Entity: Saving and Awakening with Logging
1
Task
JAVA 25 SELF, level 43, lesson 0
Locked
Player Profile: Saving the Essence, Forgetting Details
Player Profile: Saving the Essence, Forgetting Details
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION