Detailed explanation
In Java’s built-in object serialization, a class implements Serializable to allow its objects to be written to an ObjectOutputStream. By default, serialization stores eligible instance fields, excluding static and transient fields. You can customize this behaviour by declaring two private methods with exact signatures: private void writeObject(ObjectOutputStream out) throws IOException and private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException. These are serialization hooks, not methods declared by Serializable, and the serialization runtime invokes them automatically for that class.
Inside writeObject(), call out.defaultWriteObject() if you want Java to write the class’s default serializable fields, then write any additional values using methods such as writeObject() or writeInt(). Inside readObject(), call in.defaultReadObject() at the corresponding point and read additional values in exactly the same order and types. For the supplied password example, password must be transient; otherwise, defaultWriteObject() also writes its plaintext value, defeating the encryption. The matching readObject() would read the encrypted representation, decrypt it, and restore the transient field. Encryption also requires sound key management; serializing the key alongside the ciphertext defeats confidentiality.
Custom deserialization is useful for rebuilding transient caches, restoring derived state, and validating invariants before an object becomes usable. For Serializable classes, normal constructors and instance initializers are not run during reconstruction; the first non-serializable superclass’s accessible no-argument constructor is invoked. Therefore, readObject() should reject invalid state, commonly with InvalidObjectException. Declare an explicit serialVersionUID for controlled compatibility, but remember that it does not automatically make incompatible layouts compatible. Optional-data evolution needs a deliberate format and compatible reading logic.
Other customization mechanisms address different needs: writeReplace() and readResolve() can substitute objects, while Externalizable provides public writeExternal() and readExternal() methods for explicit control over the external representation and requires a public no-argument constructor. Interviewers typically expect you to distinguish these mechanisms, explain the matching write/read contract, and identify security pitfalls. Avoid deserializing untrusted data where possible; input filters can reduce risk but do not make arbitrary deserialization safe.
Key points to remember
- Implement Serializable and declare the exact private writeObject() and readObject() hook signatures.
- Use defaultWriteObject() and defaultReadObject() to retain default field handling alongside custom data.
- Read custom values in the same order and with matching types as they were written.
- Mark sensitive fields transient before writing encrypted alternatives, and validate restored state.
- Use serialVersionUID for compatibility checks; consider replacement hooks or Externalizable for different customization needs.
Common mistakes candidates make
- Calling defaultWriteObject() while leaving the password non-transient, which also serializes plaintext.
- Changing the read order or data types, or assuming serialVersionUID alone solves version compatibility.
- Trusting deserialized values without validation or assuming constructors enforce Serializable-class invariants.
Likely follow-up questions
How is Externalizable different from Serializable with custom hooks?
Externalizable extends Serializable and requires public writeExternal() and readExternal() methods, making the class responsible for explicitly saving and restoring its external state. Reconstruction invokes its public no-argument constructor, unlike ordinary Serializable-class reconstruction.
What does serialVersionUID do?
It identifies the serialization version of a class, allowing the runtime to check compatibility between the stream and the local class. A mismatch normally causes InvalidClassException, but matching identifiers do not guarantee that custom data formats or field changes are compatible.
When would you use writeReplace() and readResolve()?
Use writeReplace() to substitute a different object before serialization, such as a serialization proxy. Use readResolve() to replace a reconstructed object with another instance, such as a canonical instance; this differs from customizing individual field encoding.
Interview tip: Explain the paired hooks first, walk through the transient-password example, and finish with compatibility and security caveats.
