Backend Development 15 min read

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:

<code>public interface Serializable {
}
</code>

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:

<code>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; }
}
</code>

A test class writes an instance to a file using

ObjectOutputStream

and reads it back with

ObjectInputStream

:

<code>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(); }
    }
}
</code>

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:

<code>writeObject() → writeObject0() → writeOrdinaryObject() → writeSerialData() → invokeWriteObject() → defaultWriteFields()
</code>

During deserialization,

ObjectInputStream

follows the reverse chain:

<code>readObject() → readObject0() → readOrdinaryObject() → readSerialData() → defaultReadFields()
</code>

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.

<code>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();
    }
}
</code>

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.

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

login 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.