skip to content

What are the private writeObject and readObject methods in Java serialization, and when do you use them?

level: middleimportance: must knowfreq 62%

answer

  1. private void writeObject(ObjectOutputStream) / readObject(ObjectInputStream)
  2. Called by reflection — signature must be exact and private
  3. defaultWriteObject/defaultReadObject first, then custom state in matching order
  4. readObject runs WITHOUT the constructor — re-validate here
  5. private so subclasses don't inherit/override; per-class

basics

~20 s

They are special private methods you add to a Serializable class. Java's serialization machinery calls them automatically to let you control how the object is written to and read from a stream, instead of using the default field-by-field behavior.

solid answer

~40 s

writeObject and readObject are optional private callback methods you declare on a Serializable class with exact signatures: private void writeObject(ObjectOutputStream out) and private void readObject(ObjectInputStream in). When present, the serialization runtime calls them via reflection instead of doing the default work. Inside them you typically call out.defaultWriteObject() / in.defaultReadObject() first to handle the normal fields, then read/write any extra state manually. You use them to: serialize fields the default mechanism can't (or to skip transient ones intelligently), enforce invariants on incoming data, or maintain backward compatibility across versions. readObject is also the place to re-validate untrusted input, since deserialization bypasses constructors. The methods must be private so subclasses don't inherit or override them; each class in the hierarchy provides its own.

go deeper

for a junior

Knows these are special methods that let you control serialization, and that the class must implement Serializable.

for a middle

Can write the correct private signatures, calls defaultWriteObject/defaultReadObject, and reads/writes custom state in matching order.

for a senior

Explains the constructor-bypass security implication, validates in readObject, knows the methods are reflection-invoked and per-class, handles transient fields like the JDK collections do.

for a principal

Discusses versioning strategies (GetField/PutField, serialVersionUID), deserialization attack surface, and weighs custom hooks vs Externalizable vs avoiding Java serialization entirely (e.g. JSON/protobuf) for new designs.

## What problem this solves **Serialization** is turning a live Java object into a byte stream (so it can be saved to disk or sent over a network), and **deserialization** is rebuilding the object from those bytes. Java provides this automatically for any class that implements the marker interface `java.io.Serializable` (a marker interface has no methods; it just flags the class as opt-in). By default, Java walks every non-`transient`, non-`static` field of the object and writes them one by one — this is called **default serialization**. Sometimes default serialization is wrong or insufficient: a field might hold a value that has no meaning after restart (a cached hash, an open socket), the in-memory representation might differ from what you want on disk, or you need to validate the incoming data. **`writeObject` and `readObject` are the hooks that let you take over.** ## The exact contract You add these methods to your class with EXACTLY these signatures (the runtime finds them by reflection, so the signature must match precisely): ```java private void writeObject(ObjectOutputStream out) throws IOException; private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException; ``` - They must be `private`. This is deliberate: serialization is per-class, not inherited. Each class in an inheritance chain that wants custom behavior declares its own pair, and `private` guarantees a subclass's method never shadows the parent's. - `ObjectOutputStream` is the stream you write bytes to; `ObjectInputStream` is the one you read from. When these methods exist, the runtime calls them **instead of** the default field-walking. They are invoked via reflection by `ObjectOutputStream.writeObject(obj)` / `ObjectInputStream.readObject()`. ## defaultWriteObject / defaultReadObject Usually you don't want to abandon the default field handling entirely — you want it PLUS something extra. So the first line of your custom method calls back into the runtime: ```java private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); // write all normal (non-transient) fields out.writeInt(extraValue); // then write your custom state } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // read all normal fields back this.extraValue = in.readInt();// then your custom state, in the SAME order } ``` The order of reads in `readObject` must mirror the order of writes in `writeObject` — the stream is sequential. ## Why each use case matters 1. **Handling transient fields.** A field marked `transient` is skipped by default serialization. If that field actually needs to survive (e.g. a `HashMap` you store in a custom layout for space), you mark it `transient` and write/read it manually in these hooks. The JDK's own `HashMap` and `ArrayList` do exactly this. 2. **Validation / invariants.** Deserialization **does not call your constructor** — it allocates the object and stuffs fields in. So any check your constructor normally enforces ("age must be positive") is bypassed by an attacker crafting a malicious byte stream. `readObject` is where you re-run those checks and throw `InvalidObjectException` if they fail. This is a core security point. 3. **Versioning / backward compatibility.** If you add a field in v2, an old v1 stream won't contain it; `readObject` can default it sensibly. `ObjectInputStream.GetField` lets you read named fields defensively. ## Relationship to the rest of the family `writeObject`/`readObject` customize HOW fields are streamed but keep the same object identity and type. They are distinct from `Externalizable` (full manual control, no default behavior) and from `writeReplace`/`readResolve` (which SUBSTITUTE a different object). A class uses whichever subset it needs. ## A subtle gotcha If you call `defaultReadObject()`, fields declared `final` can still be set by the runtime (it uses unsafe field assignment), but you cannot reassign a `final` field yourself inside `readObject` — so custom-restored final fields are awkward; people often drop `final` or use `readResolve`.

  • Why must writeObject/readObject be private if they're called from outside the class?
    They are called by the serialization runtime via reflection, which can access private members, so visibility doesn't block it. Private is required so the methods are NOT inherited or overridden — serialization is per-class, and each class in the hierarchy must provide its own independent pair.
  • Where should you validate untrusted deserialized data and why?
    In readObject (after defaultReadObject), because deserialization bypasses constructors, so constructor-enforced invariants don't run. Throw InvalidObjectException on bad data. Even better, validate in ObjectInputValidation via registerValidation for whole-graph checks.

saying these in an interview costs you the question

  • Saying the methods are public or protected — they must be private
  • Claiming the constructor runs during deserialization (it does not — that's why readObject validation matters)
  • Forgetting to call defaultReadObject/defaultWriteObject and then losing the normal fields silently
  • Reading custom state in a different order than it was written

context