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.
Introduction
For a long time I only knew that a class needed to implement the
Serializableinterface 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
Serializableinterface 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
ObjectOutputStreamand 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.NotSerializableExceptionis thrown, as shown in the stack trace.
During serialization,
ObjectOutputStreamfollows this chain:
<code>writeObject() → writeObject0() → writeOrdinaryObject() → writeSerialData() → invokeWriteObject() → defaultWriteFields()
</code>During deserialization,
ObjectInputStreamfollows the reverse chain:
<code>readObject() → readObject0() → readOrdinaryObject() → readSerialData() → defaultReadFields()
</code>Two important modifiers affect serialization:
staticfields belong to the class, not the object, so their values are not saved; they reflect the current class state after deserialization.
transientfields are omitted from the serialized form; after deserialization they receive default values (null for objects, 0 for primitives).
Externalizable
Besides
Serializable, Java provides the
Externalizableinterface, which requires explicit implementation of
writeExternaland
readExternaland 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
serialVersionUIDis 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
staticand
transientfields, the differences between
Serializableand
Externalizable, and the importance of a stable
serialVersionUIDis essential for reliable object persistence.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.