Serialization and Deserialization in Java - Quiz

Total: 5 questions

1. 

What are serialization and deserialization in Java, why are they used, and how do you serialize an object?

Serialization is converting the state of an object into a sequence of bytes that can be saved to a file or a database or sent over the network. Deserialization is the reverse process: rebuilding an object with the same state from those bytes. It is used to keep program state between runs (a shopping cart, settings) and to pass objects to another process or machine (Java RMI, distributed caches, HTTP sessions in a cluster).

The class must implement the marker interface java.io.Serializable (it has no methods), otherwise NotSerializableException is thrown. The steps: create a FileOutputStream, wrap it in an ObjectOutputStream and call writeObject(obj). To read, use ObjectInputStream.readObject() and cast the result; the reading program needs the same class with a compatible serialVersionUID on its classpath.

Built-in serialization produces a binary format that only Java understands. Marshalling is a broader term — preparing data for transfer between systems (Java RMI, the JAXB Marshaller for XML). To exchange data with other systems, JSON (Jackson, Gson), XML or Protocol Buffers are usually chosen.

2. 

What happens to the fields of an object during deserialization, and how is the object graph restored?

The JVM allocates memory and fills the fields with values from the stream without calling the constructor of the serializable class. Therefore:

  • regular fields (primitives and Serializable objects, private ones included) get their values from the stream;
  • transient fields are not written and get the default value (null, 0, false), even if the field has an initializer;
  • static fields are not written: you see the current class value in this JVM;
  • fields of a non-serializable superclass are set by its no-arg constructor; if there is no accessible one — InvalidClassException: no valid constructor.

The whole graph is written — every object reachable by references, and each of them must be serializable. If two references pointed to one object, after reading they point to one object too (desCollar1 == desCollar2 is true), but only within one ObjectOutputStream. The deserialized object itself is always new: dog1 == deserializedDogs[0] is false.

3. 

What is serialVersionUID and why should you declare it explicitly?

serialVersionUID is the version number of a serializable class, written to the stream together with the object. On reading, the JVM compares the number from the stream with the number of the class on the classpath; if they differ, readObject() throws java.io.InvalidClassException: ... local class incompatible.

If the field is not declared, the JVM computes it from the class structure, and any change (even a new method) or a different compiler can produce another value. That is why the version is declared explicitly: private static final long serialVersionUID = 1L;, and since Java 14 with the @Serial annotation so that the compiler checks the declaration.

With an explicit unchanged number, compatible changes do not break reading old data: a new field gets the default value, an extra field from the stream is ignored. Change the number only when old data must deliberately become unreadable (for example, a field type has changed).

4. 

How does Externalizable differ from Serializable, and how can you partly control serialization without Externalizable?

Externalizable extends Serializable and gives the programmer full control over the format:

  • you implement writeExternal(ObjectOutput) and readExternal(ObjectInput); only what the code writes explicitly is stored, so transient does not matter;
  • on reading, the public no-arg constructor is called; without it — InvalidClassException;
  • with Serializable all non-static, non-transient fields are written automatically, and no constructor is called on reading.

Serializable is the choice almost always, Externalizable — when you need a compact or special format. Partial control comes from private writeObject(ObjectOutputStream) and readObject(ObjectInputStream) methods in the class itself: call defaultWriteObject()/defaultReadObject() inside and add your own data, for example the ring size from a transient field. For a record (Java 16+) these methods are ignored: reading calls the canonical constructor, so its checks run.

5. 

Why might an object fail to serialize or to be read back, and why is deserializing untrusted data dangerous?

Common causes:

  • NotSerializableException on writing — the graph contains an object of a class without Serializable: a field of such a type, an instance of a non-static inner or anonymous class (it holds a reference to the outer object), a lambda in a field. Fix: implement Serializable, mark the field transient or make the nested class static;
  • InvalidClassException on reading — the serialVersionUID has changed, or the non-serializable superclass has no no-arg constructor;
  • ClassNotFoundException — the class is not on the classpath; EOFException — more objects are read than were written;
  • NullPointerException after reading — the field was transient and became null.

Deserializing untrusted data is dangerous: readObject() creates objects of any serializable class on the classpath, and a crafted stream can lead to arbitrary code execution or memory exhaustion. Do not accept Java objects from outside (use JSON or Protobuf); if you must, restrict the classes with an ObjectInputFilter (JEP 290; for the whole JVM — -Djdk.serialFilter) and validate the state in readObject().

Page 1 of 1