skip to content

During deserialization, whose constructors run and whose are skipped in a hierarchy that mixes serializable and non-serializable classes?

level: seniorimportance: should knowfreq 35%

answer

  1. Serializable classes: constructors SKIPPED, fields from stream
  2. Non-serializable part: constructors RUN via first non-ser no-arg ctor
  3. Boundary = lowest non-serializable superclass
  4. Constructor invariants bypassed -> validate in readObject
  5. Side effects fire only for the non-serializable ancestors

basics

~10 s

Constructors of serializable classes in the hierarchy are skipped; their fields are filled straight from the stream. Constructors of the non-serializable part are run, starting with the no-arg constructor of the first non-serializable superclass.

solid answer

~40 s

Deserialization does not use normal object construction for the serializable classes. For every class in the chain that implements Serializable, no constructor runs; the object is allocated and those fields are restored directly from the stream. For the portion of the hierarchy that is NOT serializable (the topmost non-serializable ancestor and anything above it), Java runs the normal constructor chain starting at the no-arg constructor of the first non-serializable superclass. So in `A (non-serializable) <- B (Serializable) <- C (Serializable)`, deserializing a C runs `A()` but skips `B`'s and `C`'s constructors. This is why a non-serializable superclass needs an accessible no-arg constructor, why side effects in serializable classes' constructors do not fire on deserialize, and why invariants you normally enforce in a constructor can be bypassed - a security consideration.

go deeper

for a junior

Knows deserialization does not work exactly like calling new, and that a non-serializable parent's no-arg constructor is involved.

for a middle

States that serializable classes' constructors are skipped and the non-serializable parent's no-arg constructor runs.

for a senior

Identifies the boundary precisely, predicts which constructors fire in a mixed chain, and connects this to lost side effects and skipped invariant checks.

for a principal

Treats constructor-bypass as a security/integrity surface: enforces invariants in readObject, uses readResolve and ObjectInputFilter, and weighs avoiding native serialization entirely for untrusted input.

## Key terms **Constructor**: the special method that initializes a new object. Normally `new C()` runs `A()`, then `B()`, then `C()` up the chain. **Deserialization** rebuilds an object from bytes — and it deliberately does NOT follow that normal `new` path for serializable classes. ## The rule Given an inheritance chain, classify each class as Serializable or not. During deserialization: 1. Find the **first (lowest) non-serializable superclass** in the chain (the boundary). Everything from that class **upward** is the "non-serializable part." 2. For the non-serializable part, Java runs the **ordinary constructor chain**, entered through the **no-arg constructor** of that first non-serializable superclass. This is real construction: those constructors execute, with their side effects. 3. For every class **below** that boundary that implements Serializable, **no constructor runs at all**. The fields of those classes are populated directly from the byte stream by the serialization runtime (using internal native/`Unsafe`-style allocation), bypassing constructors entirely. ## Worked example ``` class A { A() { System.out.println("A ctor"); } } // NOT Serializable class B extends A implements Serializable { B() { System.out.println("B ctor"); } } class C extends B { C() { System.out.println("C ctor"); } } // Serializable via B ``` Serialize a `C`, then deserialize it. Output during deserialization: ``` A ctor ``` Only `A()` runs. `B()` and `C()` are **skipped** because B and C are serializable; their fields come from the stream. The boundary is `A` (the first non-serializable class), so its no-arg constructor is the entry point. Contrast with `new C()`, which prints `A ctor`, `B ctor`, `C ctor`. ## Why each part of the rule exists - **No-arg constructor on the non-serializable superclass**: since Java enters the non-serializable part via its no-arg constructor, that constructor must exist and be accessible — otherwise `InvalidClassException`. - **Serializable constructors skipped**: their state is fully described by the stream, so re-running constructors would be redundant and could even conflict with restored values. This is also why constructor side effects (logging, counters, registration) in serializable classes do **not** fire on deserialize, while those in the non-serializable parent **do**. - **Security / invariants**: because serializable classes' constructors are bypassed, any validation or invariant enforcement you put in a constructor is **not** applied on deserialization. A malicious or corrupt stream can therefore produce an object in a state the constructor would normally forbid. Mitigations include validating in `readObject` (call `defaultReadObject()` then check), using `readResolve`, or `ObjectInputFilter`. ## Quick mnemonic *Constructors run for the non-serializable part (entered via its no-arg constructor) and are skipped for the serializable part.*

  • Given A(non-ser) <- B(Serializable) <- C(Serializable), which constructors run when a C is deserialized?
    Only A's no-arg constructor runs. B's and C's constructors are skipped because both are serializable; their fields are restored from the stream.
  • Why is constructor-bypass on deserialization a security concern, and how do you guard against it?
    Validation placed in constructors is skipped, so a crafted stream can create objects in invalid states. Guard by validating in readObject after defaultReadObject(), using readResolve, making fields final/transient appropriately, and applying an ObjectInputFilter.

saying these in an interview costs you the question

  • Saying all constructors run during deserialization
  • Saying no constructors run at all (the non-serializable parent's does)
  • Assuming constructor validation protects deserialized objects
  • Confusing the boundary - it is the first NON-serializable class, not the root

context