skip to content

How does Externalizable interact with serialization edge cases like readResolve, singletons, and final fields?

level: seniorimportance: nice to knowfreq 22%

answer

  1. readResolve works with Externalizable too -> singletons survive
  2. Externalizable can't set final fields (ordinary assignment)
  3. Serializable can set finals via privileged runtime
  4. Records: constructor-based, special handling
  5. Enums: serialize by name, custom logic ignored

basics

~20 s

Externalizable still supports readResolve to swap the deserialized object for another instance, which matters for singletons. But because it constructs via a public no-arg constructor and you assign fields yourself, truly final fields can't be set in readExternal, so final fields don't work the same as with Serializable.

solid answer

~40 s

Externalizable participates in the same readResolve/writeReplace mechanism as Serializable: after readExternal runs, the runtime calls readResolve if present, letting you return a canonical instance — essential for preserving singletons across deserialization. However, Externalizable interacts poorly with final fields: deserialization creates the object with the public no-arg constructor and then your readExternal assigns fields with ordinary assignment, which the compiler forbids for final fields, so you can't restore final state through readExternal. Serializable, by contrast, sets final fields via privileged runtime field-setting. The practical guidance: for immutable or singleton types, prefer Serializable with readResolve (or avoid native serialization). Externalizable suits mutable objects with a public no-arg constructor where you want format control; it's a poor fit for deeply immutable designs. Note enums and records also have their own serialization rules that override custom logic.

go deeper

for a junior

Aware that deserialization can accidentally create a second instance of a singleton and that there's a hook to prevent it.

for a middle

Can describe readResolve for singletons and recall that Externalizable struggles with final/immutable fields.

for a senior

Explains why final fields work under Serializable but not Externalizable, and chooses the right mechanism for immutable/singleton/record/enum cases.

for a principal

Reasons about object identity, immutability invariants, and serialization-proxy patterns across the whole codebase, and sets guidance to avoid native serialization for untrusted or evolving contracts.

## Background hooks: writeReplace / readResolve Java serialization defines two optional **object-substitution hooks** that apply to **both** `Serializable` and `Externalizable` classes: - **`writeReplace()`** — called *before* writing; lets the object substitute a different object to actually serialize (e.g. a serialization proxy). - **`readResolve()`** — called *after* reading (after `readExternal` for Externalizable); lets you replace the just-deserialized object with another instance before it's handed back to the caller. ```java private Object readResolve() { return INSTANCE; } // return the canonical one ``` ## Singletons A **singleton** is a class with exactly one instance. Naive serialization breaks this: deserializing creates a **new** object, so you'd have two "singletons." `readResolve` fixes it — return the canonical `INSTANCE` and discard the freshly built one. This works with `Externalizable` too: the runtime still calls `readResolve` after `readExternal`. (For singletons, a single-element `enum` is the most robust approach, because enum serialization is handled specially and can't be subverted.) ## Final fields — the real friction point A **final field** can only be assigned once, in a constructor or initializer — the compiler forbids reassigning it elsewhere. - With **`Serializable`**, the runtime sets fields through a **privileged, reflection-based mechanism** that can write final fields directly when restoring state. So immutable classes with final fields can be serialized. - With **`Externalizable`**, restoration happens inside **your** `readExternal` using **ordinary assignment** (`this.field = in.readX()`). The compiler **rejects** assigning to a `final` field there. The object was already constructed by the no-arg constructor (which set finals to their defaults/initializers), and you can't reassign them afterward. **Consequence:** you effectively cannot restore meaningful `final` field state with Externalizable. Immutable types and Externalizable don't mix; that's a structural reason Externalizable favors **mutable** designs. ## Records A **record** is an immutable, all-final data carrier. Records have **special serialization semantics**: they serialize their components and reconstruct via the **canonical constructor**, ignoring custom readObject/readExternal logic. So Externalizable is not the tool for records — their handling is built in and constructor-based. ## Enums Enum constants serialize by **name** and are resolved back to the existing constant; custom serialization methods on enums are ignored. This is why enum singletons are unbreakable across serialization. ## Inheritance recap Unlike Serializable's per-class field handling, Externalizable provides one method pair for the whole object, so superclass state must be handled explicitly (call `super.writeExternal/readExternal`). ## Decision guidance | Scenario | Better fit | |---|---| | Singleton must survive serialization | `readResolve` (Serializable) or enum singleton | | Immutable type with final fields | `Serializable` (it can set finals) — not Externalizable | | Record | Built-in record serialization (constructor-based) | | Mutable object, want compact/fast custom format | `Externalizable` | | Cross-language / untrusted input | none of the above — Protobuf/Avro/JSON | ## Summary Externalizable keeps the `writeReplace`/`readResolve` substitution hooks, so singleton preservation still works. But because it reconstructs via a public no-arg constructor plus ordinary field assignment, it cannot restore `final` fields and is a poor match for immutable types, records, and enums — all of which have their own constructor- or name-based serialization paths.

  • Why can't readExternal restore a final field but Serializable's readObject path can?
    readExternal uses ordinary assignment, which the compiler forbids for final fields after construction; Serializable uses a privileged reflective field-setting mechanism that can write finals directly.
  • How do you keep a singleton truly single across deserialization?
    Implement readResolve to return the canonical INSTANCE (works for both Serializable and Externalizable), or use a single-element enum, whose serialization is handled specially and can't be subverted.

saying these in an interview costs you the question

  • Saying readResolve doesn't work with Externalizable (it does).
  • Claiming you can assign final fields inside readExternal.
  • Trying to use Externalizable to customize record serialization.
  • Assuming custom serialization logic on enums takes effect.
  • Forgetting deserialization can create a second 'singleton' without readResolve.

context