14.2 Object Serialization, `Serializable`, `transient`, and `serialVersionUID`

Key Takeaways

  • Object serialization converts live object graphs into a portable byte stream via ObjectOutputStream, while deserialization reconstitutes them via ObjectInputStream.

  • Classes must implement the java.io.Serializable marker interface; all reachable non-transient instance references in the object graph must also be Serializable, or NotSerializableException is thrown at runtime.

  • transient instance fields and static fields are omitted from serialization; upon deserialization, transient fields default to null, 0, or false, and constructors of the serializable class do not execute.

  • If a superclass does not implement Serializable, it must provide an accessible no-argument constructor that executes during deserialization; failing this requirement causes InvalidClassException.

  • Java records bypass custom writeObject and readObject methods, always invoking their canonical constructor during deserialization to enforce component invariants.

Last updated: October 2026

14.2 Object Serialization, Serializable, transient, and serialVersionUID

Object serialization is the mechanism by which Java converts an active object graph in memory into an ordered byte stream suitable for persistent storage on disk or transmission across a network. Deserialization is the reverse process: reconstructing the live object graph from the serialized byte stream. The objective "Serialize and de-serialize Java objects" covers serialization mechanics, including the Serializable marker interface, field eligibility (transient vs. static), superclass constructor execution, versioning via serialVersionUID, custom serialization hooks, and the specialized deserialization rules governing Java records.


Serialization Mechanics & The Serializable Interface

To serialize and deserialize objects, Java provides two high-level decorator streams in java.io:

  • ObjectOutputStream: Wraps an underlying OutputStream and writes objects using void writeObject(Object obj) throws IOException.
  • ObjectInputStream: Wraps an underlying InputStream and reconstitutes objects using Object readObject() throws IOException, ClassNotFoundException.

The Serializable Marker Interface

For an object to be serializable, its class must implement java.io.Serializable. This is a marker interface (it contains no fields and no methods). It acts as a metadata flag to the JVM indicating that the class author authorizes its internal state to be encoded into bytes.

import java.io.Serializable;

public class Employee implements Serializable {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
}

Deep Object Graph Traversal & NotSerializableException

When writeObject() is invoked, Java serializes not just the root object, but recursively traverses its entire reference graph. Every object reachable through non-transient instance fields must also implement Serializable.

If any reachable object in the object graph does not implement Serializable, the serialization process halts immediately and throws java.io.NotSerializableException at runtime, identifying the unqualified class.

public class Department implements Serializable {
    private String deptName;
    private Manager manager; // If Manager does NOT implement Serializable,
                             // serializing Department throws NotSerializableException!
}

What Gets Serialized: instance, static, and transient Fields

Understanding which fields are included in the serialized stream is a critical exam topic:

  1. Instance Fields: Included by default. The values of all non-transient instance fields are written to the stream.
  2. static Fields: Never serialized. Static fields belong to the class definition loaded in the JVM's ClassLoader, not to any individual instance. When an object is deserialized, static fields reflect whatever value is currently loaded in the receiving JVM.
  3. transient Fields: Omitted from serialization. The transient keyword marks fields containing sensitive data (such as passwords, credit card numbers, or cryptographic keys) or transient runtime state (such as file handles, thread references, or cached calculations) that should not be persisted.

Deserialization Field Initialization & Constructor Execution

When an object is deserialized via readObject():

  • Constructors of the Serializable class DO NOT run! The JVM allocates memory directly and restores instance fields from the serialized byte stream.
  • Instance initializers and field default assignments DO NOT run for the serializable class.
  • transient fields are initialized to their default type values:
    • Object references default to null.
    • Numeric primitives (byte, short, int, long, float, double) default to 0 or 0.0.
    • boolean primitives default to false.
    • char primitives default to \u0000.
import java.io.*;

public class Account implements Serializable {
    private static int globalCounter = 100; // NOT serialized
    private String accountNumber;           // Serialized
    private transient String secretPin;     // OMITTED: defaults to null on deserialization
    private transient int failedAttempts;   // OMITTED: defaults to 0 on deserialization

    public Account(String acc, String pin) {
        this.accountNumber = acc;
        this.secretPin = pin;
        this.failedAttempts = 3;
    }
}

Class Inheritance in Serialization

Inheritance introduces specific contracts depending on which classes in the hierarchy implement Serializable:

Subclasses of Serializable Superclasses

If a class extends a superclass that implements Serializable, the subclass is automatically serializable by inheritance, even if the subclass does not explicitly declare implements Serializable.

Non-Serializable Superclasses

When a serializable class extends a superclass that does NOT implement Serializable:

  1. The non-serializable superclass state is not automatically serialized.
  2. Mandatory Requirement: The non-serializable superclass must have an accessible no-argument constructor (public, protected, or package-private if in the same package).
  3. Execution during Deserialization: During deserialization, the JVM invokes the non-serializable superclass's no-argument constructor to initialize its inherited fields. The serializable subclass constructor still does not run.
  4. Failure Condition: If the non-serializable superclass lacks an accessible no-argument constructor, readObject() immediately throws java.io.InvalidClassException at runtime!
// Non-serializable superclass
class Vehicle {
    protected String registrationNumber;
    // Mandatory: Must have an accessible no-arg constructor!
    public Vehicle() {
        this.registrationNumber = "UNREGISTERED";
    }
    public Vehicle(String reg) {
        this.registrationNumber = reg;
    }
}

// Serializable subclass
class Car extends Vehicle implements Serializable {
    private String model;
    public Car(String reg, String model) {
        super(reg);
        this.model = model;
    }
}

When Car is deserialized:

  1. Vehicle() (the no-arg constructor of Vehicle) executes, setting registrationNumber to "UNREGISTERED".
  2. Car's instance field model is restored from the stream to its original serialized value.
  3. The parameterized Car(String, String) constructor is bypassed entirely.

Version Compatibility with serialVersionUID

Java serialization assigns a version number called serialVersionUID to each serializable class. This identifier verifies that the class definition loaded in the receiving JVM is compatible with the byte stream being deserialized.

private static final long serialVersionUID = 1L;

What Happens if serialVersionUID is Omitted?

If a class does not explicitly declare serialVersionUID, the serialization runtime computes a default hash value at run time from class details (class name, modifiers, implemented interfaces, field names, and method signatures).

However, relying on automatic calculation is dangerous:

  • Any trivial change to the class (adding an internal helper method, reordering fields, or recompiling with a different compiler) alters the generated hash.
  • When deserializing previously stored instances, the JVM compares the stream's UID with the current class's UID. If they mismatch, deserialization fails with java.io.InvalidClassException.
  • Explicitly declaring private static final long serialVersionUID maintains backward compatibility across class revisions.

Custom Serialization Protocols

Classes can customize their serialization process by defining special private hook methods:

private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException {
    out.defaultWriteObject(); // Serializes standard non-transient fields
    out.writeInt(computeChecksum()); // Writes custom extra data
}

private void readObject(java.io.ObjectInputStream in) 
        throws java.io.IOException, ClassNotFoundException {
    in.defaultReadObject(); // Reconstitutes standard non-transient fields
    this.checksum = in.readInt(); // Reads custom extra data
    this.calculatedHash = recalculate(); // Re-initializes transient fields
}

Rules for custom hooks:

  • Both methods must be declared private void.
  • out.defaultWriteObject() and in.defaultReadObject() must be called first to handle standard field processing.
  • writeReplace() and readResolve(): Enable object substitution. readResolve() is commonly used to protect the Singleton Pattern, ensuring that deserialization returns the existing singleton instance rather than creating a duplicate object.
public class ConfigurationManager implements Serializable {
    private static final ConfigurationManager INSTANCE = new ConfigurationManager();
    private ConfigurationManager() {}
    public static ConfigurationManager getInstance() { return INSTANCE; }

    // Guarantees singleton identity after deserialization
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

Serialization of Java Records (Java 16+)

Java records (record) represent immutable data carriers. Record serialization differs sharply from regular class serialization:

DimensionStandard Class SerializationRecord Serialization (record)
Constructor ExecutionBypassed: Fields are restored directly by JVM reflection; constructors do NOT runMandatory: Deserialization always invokes the canonical constructor
Invariant EnforcementCompact/custom constructor validation logic is skippedCompact/canonical constructor validations always execute
Custom Serialization MethodsRespects writeObject(), readObject(), readObjectNoData()Completely ignores writeObject, readObject, and readObjectNoData
serialPersistentFieldsConfigures custom field listsIgnored; components define serialized fields directly
Replacement HooksSupports writeReplace() and readResolve()Supports writeReplace() and readResolve()
import java.io.Serializable;

public record UserProfile(String username, int age) implements Serializable {
    public UserProfile {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative: " + age);
        }
    }
}

If a malicious actor crafts a corrupted byte stream with age = -5:

  • A standard class would deserialize the object with -5 because constructors are bypassed.
  • The record deserialization process passes the stream values into the canonical constructor, triggering IllegalArgumentException and rejecting the invalid state!
Test Your Knowledge

Consider the following class and execution sequence:

import java.io.*;

public class InventoryItem implements Serializable {
    public static int categoryCode = 10;
    public int itemId = 101;
    public transient String secretNotes = "Confidential";

    public static void main(String[] args) throws Exception {
        InventoryItem item = new InventoryItem();
        item.itemId = 202;
        item.secretNotes = "TopSecret";
        InventoryItem.categoryCode = 50;

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(item);
        }

        InventoryItem.categoryCode = 99;

        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
            InventoryItem restored = (InventoryItem) ois.readObject();
            System.out.println(restored.itemId + ", " + restored.secretNotes + ", " + InventoryItem.categoryCode);
        }
    }
}

What is printed when this program runs?

A

101, Confidential, 99

B

202, null, 99

C

202, Confidential, 50

D

202, TopSecret, 50

Test Your Knowledge

A class Child implements Serializable and extends Parent. Parent does NOT implement Serializable. What requirement must Parent satisfy so that Child instances can be successfully deserialized?

A

Parent must provide a private void readObject(ObjectInputStream in) method.

B

Parent must declare an explicit private static final long serialVersionUID field matching Child.

C

Parent must have an accessible no-argument constructor, which runs during deserialization.

D

Parent must be declared abstract and implement Cloneable.

Test Your Knowledge

How does deserialization of a Java record differ from deserialization of an ordinary class?

A

Records require custom private void readObject(ObjectInputStream in) methods to map component fields.

B

Records always deserialize by passing stream values into their canonical constructor, guaranteeing constructor validations execute and ignoring custom readObject methods.

C

Records cannot implement Serializable and must be converted to DTO classes prior to serialization.

D

Records deserialize by reflecting directly onto memory fields, bypassing all compact and canonical constructors.

Test Your Knowledge

What happens when ObjectInputStream.readObject() deserializes an object whose serialized byte stream contains a serialVersionUID that does not match the serialVersionUID of the local class loaded in the JVM?

A

Throws java.io.InvalidClassException at runtime.

B

Silently ignores the mismatch and sets missing fields to default values.

C

Throws java.lang.ClassCastException at runtime.

D

Throws java.io.NotSerializableException at runtime.

Sections you finish are checked off in the contents.