skip to content

How do you correctly implement writeExternal and readExternal, and what are the common pitfalls?

level: middleimportance: should knowfreq 40%

answer

  1. Read in the exact write order
  2. writeUTF for String, writeObject for arbitrary
  3. writeUTF can't take null
  4. Subclass must call super.writeExternal/readExternal
  5. Add a version int for evolution

basics

~20 s

In writeExternal you write each value you want to keep; in readExternal you read them back in exactly the same order and assign them to fields. The biggest pitfall is reading in a different order or count than you wrote.

solid answer

~40 s

writeExternal(ObjectOutput out) writes the object's state using methods like writeInt, writeUTF, and writeObject. readExternal(ObjectInput in) must read the same values, in the same order and with the matching read method, then assign them to fields. The cardinal rule is order and symmetry: the read sequence must mirror the write sequence exactly, or you corrupt data or throw exceptions. Other pitfalls: forgetting the public no-arg constructor; not handling null when using writeObject/readObject; ignoring superclass state (Externalizable is not inherited automatically, so a subclass must call super.writeExternal/readExternal or re-serialize parent fields itself); and the lack of automatic versioning, so changing the field set silently breaks old data unless you write a format version number first and branch on it in readExternal. Both methods declare IOException; readExternal also declares ClassNotFoundException for readObject.

code

java · 18 lines
java
class User implements Externalizable {
    private static final int FORMAT = 1;
    private String name;
    private int age;

    public User() {} // required public no-arg ctor

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(FORMAT);   // version tag for evolution
        out.writeUTF(name);
        out.writeInt(age);
    }
    public void readExternal(ObjectInput in) throws IOException {
        int v = in.readInt();   // read in the SAME order
        name = in.readUTF();
        age = in.readInt();
    }
}

go deeper

for a junior

Can write matching writeExternal/readExternal that store and restore a couple of primitive fields in the same order.

for a middle

Handles nulls, casts readObject results, and recalls the order-symmetry and no-arg-constructor rules.

for a senior

Manages superclass state explicitly and designs a versioned format to evolve the schema safely; reasons about failure modes (EOFException, corruption).

for a principal

Treats the hand-rolled format as a long-lived contract — compatibility policy, migration, and whether a schema-based external format is a better fit than maintaining bespoke read/write code.

## The two methods ```java public void writeExternal(ObjectOutput out) throws IOException; public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; ``` `ObjectOutput`/`ObjectInput` are interfaces (implemented by `ObjectOutputStream`/`ObjectInputStream`) offering primitive writers/readers (`writeInt`/`readInt`, `writeLong`/`readLong`, `writeUTF`/`readUTF` for strings, `writeBoolean`/`readBoolean`, …) plus `writeObject`/`readObject` for arbitrary objects. ## The cardinal rule: symmetry of order The stream is a flat, ordered sequence of bytes with **no field names**. So `readExternal` must read values **in the same order, the same count, and with the matching method** as `writeExternal` wrote them. Mismatches don't fail loudly the way a typed format would — they silently misinterpret bytes or throw `EOFException`/`StreamCorruptedException`. ```java public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(age); // 1 out.writeUTF(name); // 2 out.writeObject(tags); // 3 } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { age = in.readInt(); // 1 — same order name = in.readUTF(); // 2 tags = (List<String>) in.readObject(); // 3 — cast needed } ``` ## Pitfall 1 — the public no-arg constructor Required (see the constructor question): the runtime calls it before `readExternal`. Omit it (or any non-public visibility) and you get `InvalidClassException`. ## Pitfall 2 — nulls with writeObject/readObject `writeObject(null)` is fine and `readObject()` returns `null`, but `writeUTF` cannot write `null` (NPE). For nullable strings either guard them or use `writeObject`. Always know whether a field can be null and handle it symmetrically. ## Pitfall 3 — inheritance is not automatic Unlike `Serializable` (where each serializable class's fields are handled per class), `Externalizable` gives **one pair of methods for the whole object**. If a subclass extends an Externalizable parent, the subclass's `writeExternal`/`readExternal` must explicitly **call `super.writeExternal(out)` / `super.readExternal(in)`** (or otherwise persist the parent's fields), or parent state is lost. ## Pitfall 4 — versioning is on you `Serializable` has `serialVersionUID` and default/`readObject` hooks to manage evolution. `Externalizable` has **none of that automatically** — if you add or remove a field, old bytes no longer match your new read sequence. The standard remedy is to **write a format version number first** and branch on it: ```java private static final int V = 2; public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(V); out.writeUTF(name); if (V >= 2) out.writeUTF(email); // added in v2 } public void readExternal(ObjectInput in) throws IOException { int v = in.readInt(); name = in.readUTF(); email = v >= 2 ? in.readUTF() : null; } ``` ## Pitfall 5 — exception handling Both methods declare `IOException`; `readExternal` additionally declares `ClassNotFoundException` (thrown by `readObject` when the class of a serialized object isn't on the classpath). Don't swallow these — let them propagate so corruption is visible. ## Summary discipline Write and read in mirrored order; supply the public no-arg constructor; handle nulls; persist superclass state explicitly; embed a version tag for evolution. Externalizable trades the runtime's safety nets for raw control, so these become your responsibility.

  • Why is writing a version number first a good practice in writeExternal?
    Externalizable has no automatic versioning, so embedding a format version lets readExternal branch on it and stay backward-compatible when fields are added or removed.
  • What does ObjectInput.readObject throw that readInt does not?
    ClassNotFoundException — when the class of a serialized object isn't available on the classpath; that's why readExternal's signature declares it.

saying these in an interview costs you the question

  • Reading fields in a different order or count than written.
  • Calling writeUTF on a possibly-null String.
  • Assuming superclass fields are serialized automatically.
  • Assuming serialVersionUID-style versioning happens for free with Externalizable.
  • Forgetting that readExternal declares ClassNotFoundException.

context