11.4 Java Object Serialization and Record Serialization

Key Takeaways

  • Object serialization converts object graphs into byte streams via ObjectOutputStream and deserializes them via ObjectInputStream, requiring participating classes to implement the Serializable marker interface.
  • Fields marked transient and static are excluded from serialization; upon standard class deserialization, transient fields are initialized to primitive default values or null without executing inline initializers.
  • Deserialization of ordinary classes bypasses the serializable class's constructors and only invokes the no-argument constructor of the first non-serializable superclass, requiring an accessible no-arg constructor to avoid InvalidClassException.
  • Custom serialization hooks (writeObject, readObject, readResolve, writeReplace) allow tailored data representation and singleton identity preservation in standard classes.
  • Record serialization in Java SE 21 bypasses custom writeObject/readObject methods and unconditionally reconstructs record instances through their canonical constructor, guaranteeing that all validation rules and invariants are strictly enforced.
Last updated: September 2026

Java Object Serialization and Record Serialization

Serialization is the mechanism of transforming an in-memory Java object graph into a sequential stream of binary bytes for persistent storage on disk or transmission across network boundaries. Deserialization performs the reverse transformation, reconstructing a live, active Java object graph on the JVM heap from the serialized byte stream.

On the Oracle Certified Professional: Java SE 21 Developer (1Z0-830) exam, serialization is a heavily tested architectural topic. Candidates must master the java.io.Serializable marker interface, serialVersionUID evolution rules, transient and static field behavior, constructor invocation semantics during standard class deserialization, custom serialization hooks (writeObject, readObject, readResolve, writeReplace), the Externalizable interface, and the modern serialization protocol for Java records.


1. The Serialization Protocol & Serializable

To enable serialization for a class, it must implement the java.io.Serializable interface. Serializable is a marker interface (it declares no methods or constants), signaling to the JVM that instances of the class are safe to serialize.

public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String name;
    private double salary;
    private transient String secretPin; // Excluded from serialization
    
    public Employee(String name, double salary, String pin) {
        this.name = name;
        this.salary = salary;
        this.secretPin = pin;
    }

    public String getName() { return name; }
    public double getSalary() { return salary; }
    public String getSecretPin() { return secretPin; }
}

Serializing and Deserializing Objects

Object serialization is executed via java.io.ObjectOutputStream and java.io.ObjectInputStream:

// Writing an object graph to a binary file:
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employee.ser"))) {
    Employee emp = new Employee("Alice Johnson", 95000.0, "9876");
    oos.writeObject(emp);
}

// Reading and reconstructing the object graph from the binary file:
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("employee.ser"))) {
    Employee emp = (Employee) ois.readObject();
    System.out.println("Name:   " + emp.getName());      // "Alice Johnson"
    System.out.println("Salary: " + emp.getSalary());    // 95000.0
    // Transient field receives primitive/reference default value:
    System.out.println("PIN:    " + emp.getSecretPin()); // null
}

The NotSerializableException

[!WARNING] If an object implementing Serializable contains a reference to an object that does not implement Serializable, invoking ObjectOutputStream.writeObject() throws java.io.NotSerializableException at runtime! To prevent this exception, non-serializable fields must be explicitly marked with the transient keyword or must hold a null reference at the time of serialization.


2. Field Modifiers: transient and static

  • static Fields: Belong to the class definition rather than any individual object instance. Static fields are never serialized. When an object is deserialized, static fields reflect whatever value is currently loaded in the target JVM's class definition.
  • transient Fields: Explicitly excluded from the serialized stream. They are used for sensitive credentials (passwords, cryptographic keys), temporary cache buffers, or non-serializable system resources (such as database connections or thread handles).

The Transient Default Initialization Trap

When an ordinary Serializable class is deserialized:

  1. transient reference types are initialized to null.
  2. transient numeric primitives (byte, short, int, long, float, double) are initialized to 0 (0.0).
  3. transient boolean fields are initialized to false.
  4. transient char fields are initialized to '\u0000'.

[!IMPORTANT] Inline Field Initializers Do NOT Run: During standard class deserialization, inline field initializers (e.g., private transient int maxAttempts = 5;) and instance initializer blocks are NOT executed. The transient field will have the default value 0, not 5!


3. Class Evolution and serialVersionUID

serialVersionUID is a 64-bit hash (long) used as a version identifier for a Serializable class:

private static final long serialVersionUID = 1L;

Why Declare serialVersionUID Explicitly?

If a Serializable class does not declare an explicit serialVersionUID, the Java compiler automatically computes a default UID based on the class name, implemented interfaces, fields, and method signatures. Any subsequent change to the source code (such as adding a private helper method or reordering fields) changes the generated UID hash.

InvalidClassException

During deserialization, ObjectInputStream compares the serialVersionUID stored in the serialized byte stream with the serialVersionUID of the class currently loaded in the JVM. If the two identifiers do not match, deserialization immediately aborts by throwing java.io.InvalidClassException.


4. Object Deserialization & Constructor Invocation Rules

One of the most frequently tested topics on the 1Z0-830 exam is the exact sequence of constructor invocations during deserialization of standard classes:

The 4 Golden Deserialization Rules for Standard Classes

  1. Constructors of Serializable classes are NEVER executed during deserialization! The JVM allocates raw uninitialized heap memory for the object and injects field values directly from the stream using internal reflection.
  2. The JVM traverses up the class inheritance hierarchy to locate the first NON-SERIALIZABLE superclass.
  3. The JVM invokes the NO-ARGUMENT constructor of that first non-serializable superclass.
  4. If that first non-serializable superclass does not declare an accessible no-argument constructor (for example, if it only defines parameterized constructors), the JVM throws java.io.InvalidClassException at runtime during deserialization!
// Non-serializable superclass MUST have an accessible no-arg constructor:
public class Person {
    private String species;
    
    public Person() { // Required for subclass deserialization!
        this.species = "Homo sapiens";
        System.out.println("Person no-arg constructor executed");
    }
    
    public Person(String species) {
        this.species = species;
    }
}

public class Manager extends Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String department;
    
    public Manager(String dept) {
        super("Human");
        this.department = dept;
        System.out.println("Manager constructor executed");
    }
}

// When deserializing a serialized Manager object:
// 1. Person() no-arg constructor IS executed!
// 2. Manager(String) constructor IS NOT executed!

5. Custom Serialization Hooks for Standard Classes

Standard classes can customize their serialization and deserialization formats by declaring special private methods with exact signatures:

public class SecureAccount implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String accountNumber;
    private transient String password;
    
    // Custom serialization method
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // Serializes all non-transient fields normally
        // Write custom encrypted/masked representation of transient field:
        String encryptedPass = encrypt(this.password);
        out.writeUTF(encryptedPass);
    }
    
    // Custom deserialization method
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // Deserializes all non-transient fields normally
        // Read and restore custom encrypted transient field:
        String encryptedPass = in.readUTF();
        this.password = decrypt(encryptedPass);
    }
    
    // Invoked if class hierarchy has changed and stream has no data for this class
    private void readObjectNoData() throws ObjectStreamException {
        this.password = "DEFAULT_PASSWORD";
    }
    
    // Designates an alternate object to be serialized in place of this object
    private Object writeReplace() throws ObjectStreamException {
        return new AccountProxy(this.accountNumber);
    }
    
    // Replaces the deserialized object with an existing instance (e.g. Singleton Pattern)
    private Object readResolve() throws ObjectStreamException {
        return AccountRegistry.getCanonicalInstance(this.accountNumber);
    }
    
    private String encrypt(String raw) { return raw != null ? "ENC:" + raw : ""; }
    private String decrypt(String enc) { return enc.startsWith("ENC:") ? enc.substring(4) : enc; }
}

The Role of readResolve() in the Singleton Pattern

When a singleton instance is serialized and subsequently deserialized via readObject(), the JVM standard deserializer creates a new, distinct instance on the heap, violating the core invariant of the Singleton design pattern. Implementing readResolve() allows the class to discard the newly deserialized instance and return the existing singleton reference instead:

public class DatabasePool implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final DatabasePool INSTANCE = new DatabasePool();
    
    private DatabasePool() {}
    public static DatabasePool getInstance() { return INSTANCE; }
    
    // Preserves Singleton Identity during Deserialization:
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

6. The java.io.Externalizable Interface

Externalizable is a sub-interface of Serializable that provides complete, manual control over the binary format:

public interface Externalizable extends Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

Critical Exam Rules for Externalizable

  1. Requires Public No-Arg Constructor: Unlike standard Serializable classes (where constructors are bypassed), an Externalizable class MUST declare a public no-argument constructor.
  2. Constructor Invocation: During deserialization, the JVM first invokes the public no-argument constructor of the externalizable class itself, and then calls readExternal(in) on that newly created instance.
  3. No Automatic Field Storage: The JVM does not automatically write any field data. All fields (even non-transient ones) must be explicitly written in writeExternal() and read in readExternal() in identical order.

7. Modern Record Serialization in Java SE 21

Java records introduce a completely redesigned, secure serialization protocol that fundamentally differs from standard class serialization.

public record CustomerOrder(String orderId, double amount, int itemCount) implements Serializable {
    // Compact constructor enforces validation invariants:
    public CustomerOrder {
        Objects.requireNonNull(orderId, "orderId cannot be null");
        if (amount < 0.0) {
            throw new IllegalArgumentException("Order amount cannot be negative");
        }
        if (itemCount <= 0) {
            throw new IllegalArgumentException("Item count must be positive");
        }
    }
}

The 5 Core Rules of Record Serialization

  1. Always Invokes the Canonical Constructor: Unlike standard classes (which bypass constructors), record deserialization ALWAYS invokes the canonical constructor, passing the deserialized stream components as arguments.
  2. Strict Invariant and Validation Enforcement: Because the canonical constructor is guaranteed to execute, any validation checks, sanitization logic, or defensive copies defined in compact/canonical constructors are strictly enforced during deserialization. Tampered or malicious serialized byte streams cannot bypass record integrity constraints!
  3. Custom Methods are Completely Ignored: Declaring writeObject(), readObject(), readObjectNoData(), or serialPersistentFields inside a record has zero effect. The JVM serialization framework completely ignores them.
  4. serialVersionUID Defaults to 0L: The serialVersionUID requirement is waived for records. The default UID is 0L, and changes in component order or adding/removing record components do not trigger InvalidClassException based on UID mismatch.
  5. readResolve and writeReplace Supported: Records support writeReplace() and readResolve() (for instance, to substitute a proxy or canonical cached instance).

8. Serialization Architecture Comparison Matrix

FeatureStandard Class (Serializable)Externalizable ClassRecord (Record implements Serializable)
Deserialization MechanismField injection via reflectionreadExternal() callbackCanonical Constructor Invocation
Constructor ExecutedFirst non-serializable superclass no-argClass's own public no-arg constructorRecord's Canonical Constructor
Validation EnforcementBypassed by defaultBypassed (inside readExternal)Strictly Enforced by Constructor
writeObject / readObjectSupportedIgnored (uses writeExternal)Completely Ignored
readResolve / writeReplaceSupportedSupportedSupported
serialVersionUID CheckEnforced (UID mismatch throws exception)EnforcedWaived (Defaults to 0L)
transient KeywordExcludes fieldN/A (all serialization is manual)Not permitted on record components
Loading diagram...
Standard Class vs. Record Deserialization Lifecycle
Test Your Knowledge

A class Account is defined as follows:

public class Account implements Serializable {
    private static final long serialVersionUID = 1L;
    private String accountNumber = "ACT-100";
    private transient double balance = 500.0;
    private transient String securityToken = "TOKEN-XYZ";
}
If an instance of Account is serialized to disk and subsequently deserialized into a new JVM instance, what will be the values of accountNumber, balance, and securityToken in the deserialized object?

A
B
C
D
Test Your Knowledge

Given the following class hierarchy:

class Vehicle {
    public Vehicle(String type) {
        System.out.print("V1 ");
    }
}

class Car extends Vehicle implements Serializable {
    private static final long serialVersionUID = 1L;
    public Car() {
        super("Sedan");
        System.out.print("C1 ");
    }
}
What happens when an existing serialized Car object is deserialized via ObjectInputStream.readObject()?

A
B
C
D
Test Your Knowledge

A developer creates a Java 21 record for storing user credentials:

public record UserCredential(String username, int pinCode) implements Serializable {
    public UserCredential {
        if (pinCode < 1000 || pinCode > 9999) {
            throw new IllegalArgumentException("PIN must be 4 digits");
        }
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new UnsupportedOperationException("Custom reading not allowed");
    }
}
If a serialized stream containing valid record component data (username = "admin", pinCode = 4321) is deserialized via ois.readObject(), what is the outcome?

A
B
C
D
Test Your Knowledge

In standard Java object serialization, what is the primary purpose of defining a private Object readResolve() method inside a singleton class?

A
B
C
D