Unlocking Java Serialization: Theory, Practice, and Common Pitfalls

Explore Java's built‑in serialization mechanism from its origins in JDK 1.1, learn how to implement Serializable and Externalizable, see practical code examples, understand the impact of static and transient fields, and master serialVersionUID to avoid deserialization errors.

macrozheng
macrozheng
macrozheng
Unlocking Java Serialization: Theory, Practice, and Common Pitfalls

Introduction

For a long time I only knew that a class needed to implement the Serializable interface to be serializable, but I never dug deeper because the basic usage seemed enough.

Theory

Java serialization was introduced in JDK 1.1 as a pioneering feature that converts Java objects into byte arrays for storage or transmission, and can later reconstruct the original object state.

The idea is to "freeze" an object's state to disk or over the network, and later "thaw" it back into a usable Java object.

The Serializable interface is defined as an empty marker:

public interface Serializable {
}

Even though it contains no methods, implementing it signals that instances of the class can be serialized and deserialized.

Practical Example

A simple class with two fields and standard getters/setters is created:

class Wanger {
    private String name;
    private int age;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

A test class writes an instance to a file using ObjectOutputStream and reads it back with ObjectInputStream:

public class Test {
    public static void main(String[] args) {
        Wanger wanger = new Wanger();
        wanger.setName("王二");
        wanger.setAge(18);
        System.out.println(wanger);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"))) {
            oos.writeObject(wanger);
        } catch (IOException e) { e.printStackTrace(); }
        // modify static field before deserialization
        Wanger.pre = "不沉默";
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")))) {
            Wanger wanger1 = (Wanger) ois.readObject();
            System.out.println(wanger1);
        } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); }
    }
}

If the class does not implement Serializable, a java.io.NotSerializableException is thrown, as shown in the stack trace.

During serialization, ObjectOutputStream follows this chain:

writeObject() → writeObject0() → writeOrdinaryObject() → writeSerialData() → invokeWriteObject() → defaultWriteFields()

During deserialization, ObjectInputStream follows the reverse chain:

readObject() → readObject0() → readOrdinaryObject() → readSerialData() → defaultReadFields()

Two important modifiers affect serialization: static fields belong to the class, not the object, so their values are not saved; they reflect the current class state after deserialization. transient fields are omitted from the serialized form; after deserialization they receive default values (null for objects, 0 for primitives).

Externalizable

Besides Serializable, Java provides the Externalizable interface, which requires explicit implementation of writeExternal and readExternal and a public no‑argument constructor.

class Wanger implements Externalizable {
    private String name;
    private int age;

    public Wanger() { }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
    }
}

If the no‑arg constructor is missing, deserialization fails with java.io.InvalidClassException.

serialVersionUID

The serialVersionUID is a version identifier that must match between the serialized stream and the local class definition. If they differ, deserialization throws InvalidClassException.

Three common ways to define it:

Explicit constant, e.g., private static final long serialVersionUID = 1L; Randomly generated value (IDE‑generated), e.g.,

private static final long serialVersionUID = -2095916884810199532L;

Suppress the warning with @SuppressWarnings("serial"), which lets the compiler generate a synthetic UID.

Changing the UID after objects have been persisted breaks compatibility, as demonstrated by the error messages when the UID is altered.

Conclusion

Java serialization, despite its seemingly empty marker interface, offers a rich set of behaviors. Understanding the serialization chain, the role of static and transient fields, the differences between Serializable and Externalizable, and the importance of a stable serialVersionUID is essential for reliable object persistence.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javaserializationserializableExternalizableObjectInputStreamObjectOutputStream
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.