Serialization vs Externalization
Serialization vs Externalization
This section's previous two articles covered Serializable and Externalizable in depth on their own. This article puts them side by side, comparing the same small class implemented both ways, to make the actual tradeoff — automatic convenience against manual control — concrete rather than abstract.
What Is the Difference Between Serializable and Externalizable?
Both persist a Java object to a byte stream and back, but they sit at opposite ends of the same tradeoff — one automatic, one entirely manual.
| Aspect | Serializable | Externalizable |
|---|---|---|
| Interface | Marker interface, no methods | Requires writeExternal() and readExternal() |
| Field handling | Every non-transient field, automatically | Only what the class explicitly writes |
| Constructor | None required — fields set via reflection | Public no-arg constructor is mandatory |
| Superclass fields | Included automatically through the hierarchy | Requires an explicit super call in both methods |
| Format control | None — determined by the JVM | Complete — the class defines the byte layout |
| Typical output size | Larger, due to field-name and type metadata | Smaller, since no field metadata is written |
| Code required | Minimal | More — every field is handled by hand |
Both approaches produce a working round-trip for the same simple class — the difference is in what each one requires and what it costs.
1// File: CacheEntrySerializable.java
2import java.io.Serializable;
3
4public class CacheEntrySerializable implements Serializable {
5 private static final long serialVersionUID = 1L;
6 private String key;
7 private String value;
8
9 public CacheEntrySerializable(String key, String value) {
10 this.key = key;
11 this.value = value;
12 }
13
14 public String getKey() { return key; }
15 public String getValue() { return value; }
16}1// File: CacheEntryExternalizable.java
2import java.io.*;
3
4public class CacheEntryExternalizable implements Externalizable {
5 private String key;
6 private String value;
7
8 public CacheEntryExternalizable() {}
9
10 public CacheEntryExternalizable(String key, String value) {
11 this.key = key;
12 this.value = value;
13 }
14
15 @Override
16 public void writeExternal(ObjectOutput out) throws IOException {
17 out.writeUTF(key);
18 out.writeUTF(value);
19 }
20
21 @Override
22 public void readExternal(ObjectInput in) throws IOException {
23 key = in.readUTF();
24 value = in.readUTF();
25 }
26
27 public String getKey() { return key; }
28 public String getValue() { return value; }
29}CacheEntrySerializable needed no constructor beyond the one already required for normal use, and no serialization-specific methods at all. CacheEntryExternalizable needed an additional public no-arg constructor and two extra methods just to persist the same two fields — the cost of the control Externalizable provides.
How the Two Actually Differ Internally
One sentence before the diagram: the difference in output size comes down to exactly what each mechanism writes into the stream alongside the actual data.
Serializable's stream for CacheEntrySerializable: [class descriptor: name "CacheEntrySerializable", serialVersionUID] [field descriptor: "key", type String] [field descriptor: "value", type String] [actual data: "session:42", "active"] Externalizable's stream for CacheEntryExternalizable: [class descriptor: name "CacheEntryExternalizable", serialVersionUID] [marker: "custom format, defer to writeExternal()"] [actual data: "session:42", "active"] <-- no per-field metadata at all
Serializable's automatic mechanism decides the format entirely — every non-transient field, in a JVM-determined layout, with no way to change it, and its class descriptor includes each field's name and type as part of the stream. Externalizable puts the class in charge, at the cost of writing and reading every field by hand, in an order the class itself must keep consistent, but its descriptor only records that a custom format is in use.
Serializable needs nothing beyond the class's normal constructors, since fields are restored directly via reflection. Externalizable requires a public no-arg constructor, since ObjectInputStream must construct the object before readExternal() has anything to populate, covered in depth in this section's dedicated Externalization article. A Serializable class's superclass fields are also included automatically, provided the superclass is also serializable, while an Externalizable class must call its superclass's writeExternal() and readExternal() explicitly — there is no automatic chaining at all.
Treat the choice as effectively permanent once data has been persisted with one mechanism — switching a class from Serializable to Externalizable, or back, changes the byte format entirely and breaks compatibility with anything already serialized under the old approach.
Real-World Example
The same simple cache entry, serialized both ways, shows the same correct round-trip result alongside the size difference the extra manual control buys.
1// File: SerializationComparisonDemo.java
2import java.io.*;
3import java.nio.file.*;
4
5public class SerializationComparisonDemo {
6 public static void main(String[] args) throws IOException, ClassNotFoundException {
7 CacheEntrySerializable serializableEntry = new CacheEntrySerializable("session:42", "active");
8 Path serializableFile = Files.createTempFile("cache-serializable", ".ser");
9 try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(serializableFile))) {
10 out.writeObject(serializableEntry);
11 }
12
13 CacheEntryExternalizable externalizableEntry = new CacheEntryExternalizable("session:42", "active");
14 Path externalizableFile = Files.createTempFile("cache-externalizable", ".ser");
15 try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(externalizableFile))) {
16 out.writeObject(externalizableEntry);
17 }
18
19 long serializableSize = Files.size(serializableFile);
20 long externalizableSize = Files.size(externalizableFile);
21
22 CacheEntrySerializable restoredSerializable;
23 try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(serializableFile))) {
24 restoredSerializable = (CacheEntrySerializable) in.readObject();
25 }
26
27 CacheEntryExternalizable restoredExternalizable;
28 try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(externalizableFile))) {
29 restoredExternalizable = (CacheEntryExternalizable) in.readObject();
30 }
31
32 System.out.println("Serializable round-trip: " + restoredSerializable.getKey() + "=" + restoredSerializable.getValue());
33 System.out.println("Externalizable round-trip: " + restoredExternalizable.getKey() + "=" + restoredExternalizable.getValue());
34 System.out.println("Externalizable file is smaller: " + (externalizableSize < serializableSize));
35
36 Files.delete(serializableFile);
37 Files.delete(externalizableFile);
38 }
39}Output:
Serializable round-trip: session:42=active
Externalizable round-trip: session:42=active
Externalizable file is smaller: true
Both restore the exact same data correctly — the difference is entirely in the file size and the code required to get there. A mistake that appears often in fresher pull requests is picking Externalizable for a simple data class purely because it "sounds more efficient," without weighing the extra code and the manual field-order discipline it demands. For a class this simple, the handful of bytes Externalizable saves rarely justifies the added maintenance burden — Serializable is the better default, and Externalizable earns its keep only when the format itself, not just its size, genuinely needs to be controlled. Whichever mechanism is chosen, every method involved declares or can throw IOException or ClassNotFoundException, tying both approaches to exception handling equally.
Best Practices
Default to Serializable for ordinary data classes — the code savings are substantial, and the automatic mechanism is correct and well-tested for the common case.
Reach for Externalizable only when a specific, articulable reason exists: a custom wire format, a measured performance requirement, or fields that need transformation during persistence beyond what transient can express.
Treat the choice as effectively permanent once data has been persisted with one mechanism — switching a class from Serializable to Externalizable, or back, changes the byte format entirely and breaks compatibility with anything already serialized under the old approach.
Document the decision when Externalizable is chosen, since the reasoning — usually performance or format control — is not obvious from the code itself the way Serializable's simplicity is.
Common Mistakes
Choosing Externalizable by default under the assumption that manual control is always better engineering, without a concrete requirement driving that choice, adds ongoing maintenance cost — every future field addition needs a matching, correctly-ordered change in both writeExternal() and readExternal() — for a benefit that, as this article's real-world example shows, is often just a modest reduction in file size.
Assuming a class can be freely switched between Serializable and Externalizable without consequence overlooks that the two produce fundamentally different byte formats. Data already persisted under one mechanism cannot be read back correctly after the class has been changed to the other — any migration needs an explicit conversion step, not just a code change.
Interview Questions
Q1. What is the fundamental difference in how Serializable and Externalizable determine what gets persisted?
Serializable automatically persists every non-transient field via reflection, with no code required beyond the marker interface itself. Externalizable persists only what writeExternal() explicitly writes, requiring the class to handle every field by hand. The nuance interviewers listen for is whether you frame this as a genuine tradeoff, not simply "one is better."
Q2. Which of the two requires a public no-arg constructor, and why?
Externalizable does. ObjectInputStream must construct an instance of the class before readExternal() has anything to populate, unlike Serializable, which restores fields directly via reflection with no constructor involved.
Q3. Which of the two automatically includes a superclass's fields, and which requires an explicit call?
Serializable includes a serializable superclass's fields automatically as part of the class hierarchy. Externalizable requires the subclass to call the superclass's writeExternal() and readExternal() explicitly — there is no automatic chaining at all. This is a common follow-up question specifically because it's a real, easy-to-miss bug source.
Q4. Which format is generally smaller, and why?
Externalizable, generally, since its class descriptor does not need to record each field's name and type the way Serializable's automatic mechanism does — Externalizable simply defers to whatever writeExternal() writes, with no per-field metadata involved. A strong answer connects this back to the actual bytes written in the stream, not just a vague claim about efficiency.
Q5. If a class is changed from Serializable to Externalizable, can it still read data serialized by the old version?
No. The two produce fundamentally different byte formats, so data serialized under one mechanism cannot be correctly deserialized by a class that has since switched to the other — this needs to be treated as a breaking change requiring explicit data migration. Product-company interviewers listen for whether you'd flag this as a migration risk before it ships, not after.
Q6. Which approach involves less code to write and maintain for a simple data class?
Serializable, by a wide margin — it needs nothing beyond the marker interface itself, while Externalizable requires an additional constructor and two methods that must be kept in sync with the class's fields by hand.
Q7. In what situation would Externalizable's extra control genuinely be worth the additional code?
When the exact byte format matters for a specific reason — a custom binary protocol, a measured and significant performance requirement, or a need to transform or selectively exclude fields in a way transient alone cannot express. The nuance being tested is whether you can articulate a concrete justification rather than defaulting to Externalizable out of habit.
FAQs
Is one of Serializable or Externalizable considered "deprecated" or "legacy" compared to the other?
No, neither is deprecated — they serve different purposes, and both remain fully supported. Serializable is simply the more commonly used default for everyday cases.
Does Externalizable still use ObjectOutputStream and ObjectInputStream?
Yes. Externalizable extends Serializable, and an Externalizable object is still written with ObjectOutputStream.writeObject() and read with ObjectInputStream.readObject() — only the internal handling of the object's fields differs.
Can a single class implement both Serializable and Externalizable at once?
There is no need to write both explicitly — since Externalizable already extends Serializable, implementing Externalizable alone is sufficient, and Externalizable's rules take over completely for that class.
Which is generally recommended as the default choice for a new class?
Serializable, unless a specific, concrete reason exists to reach for Externalizable — the code savings and reduced maintenance burden generally outweigh the format-size benefit for most classes.
Does the choice between them affect how the class's fields are declared?
Not directly — both work with ordinary fields of any serializable type. The difference shows up in how those fields are persisted and restored, not in how they are declared within the class.
Is transient relevant to both approaches equally?
No. transient matters for Serializable's automatic mechanism, telling it to skip a field. For Externalizable, a field is simply omitted from writeExternal()'s body if it should not be persisted — transient has no effect on Externalizable's behavior at all.
Do both approaches have the same security considerations around untrusted input?
Yes. Deserializing data from an untrusted source carries the same risk regardless of which mechanism controls the format, since both ultimately reconstruct live Java objects from a byte stream — neither should be used to deserialize input received from an untrusted client.
Summary
Serializable and Externalizable solve the same problem — turning a Java object into bytes and back — with opposite philosophies: automatic and convenient against manual and controlled. This article's side-by-side cache entry example showed both producing an identical, correct round-trip, differing mainly in file size and in how much code each one demanded to get there.
The habit worth carrying forward from this section as a whole is defaulting to Serializable for ordinary data classes, and reaching for Externalizable only when a concrete, articulable reason — format control, measured performance, or field-level transformation — actually justifies the extra code and the discipline it requires.