Serialization
Serialization
Serialization converts an entire Java object — and every object it references — into a byte stream that can be written to a file, sent across a network, or cached, and later reconstructed back into a live object graph. Where this section's earlier articles focused on reading and writing text, serialization persists actual Java objects, fields and all, without manually writing each one out by hand.
What Is Serialization?
Serialization is the process of flattening a live object — and every object it references — into a linear sequence of bytes that fully describes how to rebuild it later, without any manual field-by-field writing. Saving an object's state by hand means writing each field out individually and parsing them back in the same order — workable for two fields, tedious and fragile for anything larger.
1// File: BeforeSerialization.java
2import java.io.IOException;
3import java.nio.file.*;
4import java.util.List;
5
6public class BeforeSerialization {
7
8 static class Customer {
9 String name;
10 int loyaltyPoints;
11 Customer(String name, int loyaltyPoints) {
12 this.name = name;
13 this.loyaltyPoints = loyaltyPoints;
14 }
15 }
16
17 public static void main(String[] args) throws IOException {
18 Customer customer = new Customer("Ananya", 450);
19 Path tempFile = Files.createTempFile("before-serial", ".txt");
20
21 Files.writeString(tempFile, customer.name + "\n" + customer.loyaltyPoints);
22
23 List<String> lines = Files.readAllLines(tempFile);
24 Customer restored = new Customer(lines.get(0), Integer.parseInt(lines.get(1)));
25
26 System.out.println(restored.name + ": " + restored.loyaltyPoints);
27
28 Files.delete(tempFile);
29 }
30}Output:
Ananya: 450
Implementing Serializable and using ObjectOutputStream / ObjectInputStream reconstructs the entire object automatically, field by field, with no manual parsing at all.
1// File: AfterSerialization.java
2import java.io.*;
3import java.nio.file.*;
4
5public class AfterSerialization {
6
7 static class Customer implements Serializable {
8 String name;
9 int loyaltyPoints;
10 Customer(String name, int loyaltyPoints) {
11 this.name = name;
12 this.loyaltyPoints = loyaltyPoints;
13 }
14 }
15
16 public static void main(String[] args) throws IOException, ClassNotFoundException {
17 Customer customer = new Customer("Ananya", 450);
18 Path tempFile = Files.createTempFile("after-serial", ".ser");
19
20 try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(tempFile))) {
21 out.writeObject(customer);
22 }
23
24 Customer restored;
25 try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(tempFile))) {
26 restored = (Customer) in.readObject();
27 }
28
29 System.out.println(restored.name + ": " + restored.loyaltyPoints);
30
31 Files.delete(tempFile);
32 }
33}Output:
Ananya: 450
Serializable is a marker interface — it declares no methods at all. Implementing it simply tells ObjectOutputStream that instances of the class are allowed to be serialized.
How It Works Internally
One sentence before the diagram: ObjectOutputStream walks the object graph reachable from the object being written, flattening every field of every reachable object into one linear byte stream, and ObjectInputStream reverses that process exactly.
Live object graph (in memory) Byte stream (on disk / network)
ShoppingCart [class descriptor: ShoppingCart]
items -> ArrayList [field: items]
CartItem("Mouse", 1, 799.0) [class descriptor: ArrayList]
CartItem("Cable", 2, 249.0) [class descriptor: CartItem]
[fields: "Mouse", 1, 799.0]
[class descriptor: CartItem]
[fields: "Cable", 2, 249.0]
writeObject() flattens the graph -------> readObject() rebuilds it, walking
top to bottom, depth-first the stream in the same order
A field marked transient is skipped entirely during serialization and comes back with its type's default value — null for an object reference — after deserialization, regardless of what it held before.
1// File: TransientFieldExample.java
2import java.io.*;
3import java.nio.file.*;
4
5public class TransientFieldExample {
6
7 static class Session implements Serializable {
8 String username;
9 transient String temporaryToken;
10
11 Session(String username, String temporaryToken) {
12 this.username = username;
13 this.temporaryToken = temporaryToken;
14 }
15 }
16
17 public static void main(String[] args) throws IOException, ClassNotFoundException {
18 Session session = new Session("vikram", "tok-9f8e7d");
19 Path tempFile = Files.createTempFile("transient-demo", ".ser");
20
21 try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(tempFile))) {
22 out.writeObject(session);
23 }
24
25 Session restored;
26 try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(tempFile))) {
27 restored = (Session) in.readObject();
28 }
29
30 System.out.println("Username: " + restored.username);
31 System.out.println("Token: " + restored.temporaryToken);
32
33 Files.delete(tempFile);
34 }
35}Output:
Username: vikram
Token: null
username survives the round-trip because it is an ordinary serializable field. temporaryToken comes back null because it was marked transient — deserialization never attempts to restore it, which is exactly the right behavior for something like a short-lived token that should not be persisted at all.
Every Serializable class should declare an explicit private static final long serialVersionUID. Without one, the JVM computes a value automatically from the class's structure, and even a harmless-looking change like adding a method can shift it, breaking compatibility with previously serialized data and throwing InvalidClassException.
Real-World Example
An e-commerce application persists a shopping cart to disk so a user's cart survives an application restart, serializing a ShoppingCart that itself holds a List of CartItem objects — exactly the recursive graph-flattening this article's internals section describes.
1// File: CartItem.java
2import java.io.Serializable;
3
4public class CartItem implements Serializable {
5 private static final long serialVersionUID = 1L;
6
7 private final String productName;
8 private final int quantity;
9 private final double price;
10
11 public CartItem(String productName, int quantity, double price) {
12 this.productName = productName;
13 this.quantity = quantity;
14 this.price = price;
15 }
16
17 public String getProductName() { return productName; }
18 public int getQuantity() { return quantity; }
19 public double getPrice() { return price; }
20}1// File: ShoppingCart.java
2import java.io.Serializable;
3import java.util.*;
4
5public class ShoppingCart implements Serializable {
6 private static final long serialVersionUID = 1L;
7
8 private final List<CartItem> items = new ArrayList<>();
9
10 public void addItem(CartItem item) {
11 items.add(item);
12 }
13
14 public double total() {
15 return items.stream().mapToDouble(item -> item.getPrice() * item.getQuantity()).sum();
16 }
17
18 public int itemCount() {
19 return items.size();
20 }
21}1// File: ShoppingCartPersistenceDemo.java
2import java.io.*;
3import java.nio.file.*;
4
5public class ShoppingCartPersistenceDemo {
6 public static void main(String[] args) throws IOException, ClassNotFoundException {
7 ShoppingCart cart = new ShoppingCart();
8 cart.addItem(new CartItem("Wireless Mouse", 1, 799.0));
9 cart.addItem(new CartItem("USB-C Cable", 2, 249.0));
10
11 Path cartFile = Files.createTempFile("cart", ".ser");
12
13 try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(cartFile))) {
14 out.writeObject(cart);
15 }
16
17 ShoppingCart restoredCart;
18 try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(cartFile))) {
19 restoredCart = (ShoppingCart) in.readObject();
20 }
21
22 System.out.println("Items: " + restoredCart.itemCount());
23 System.out.println("Total: " + restoredCart.total());
24
25 Files.delete(cartFile);
26 }
27}Output:
Items: 2
Total: 1297.0
ArrayList, which ShoppingCart uses internally, is itself Serializable, so the entire list of CartItem objects is serialized recursively along with the cart — nothing about the nested structure needed special handling. A mistake that appears often in fresher pull requests is adding a field of a non-serializable type — a database connection, a file handle, a thread — to a class that implements Serializable, only discovering the problem when writeObject() throws at runtime. Every field an object graph reaches during serialization needs to itself be serializable or marked transient, which is exactly why ShoppingCart's only fields here are a String, primitives, and a List of another serializable class.
Best Practices
Always declare an explicit serialVersionUID rather than relying on the JVM's automatically computed value, so a harmless structural change to the class does not unexpectedly break compatibility with previously serialized data.
Mark fields transient whenever their value is not meaningful to persist — a cache, a live connection, a short-lived token — exactly as TransientFieldExample does above.
Keep a serializable class's fields limited to simple, genuinely serializable types, and verify that every referenced class in the object graph is serializable too before relying on it in production.
Consider whether serialization is the right persistence mechanism at all for long-term storage — its binary format is tied closely to the exact class definitions involved and to Java itself, making it a poor fit for long-term archival or cross-language data exchange compared to a format like JSON.
Common Mistakes
Including a field whose type is not serializable throws NotSerializableException the moment the object graph reaches it during serialization.
1// File: NotSerializableMistake.java
2import java.io.*;
3import java.nio.file.*;
4
5public class NotSerializableMistake {
6
7 static class NonSerializableResource {
8 String label = "resource";
9 }
10
11 static class Holder implements Serializable {
12 String name;
13 NonSerializableResource resource;
14
15 Holder(String name, NonSerializableResource resource) {
16 this.name = name;
17 this.resource = resource;
18 }
19 }
20
21 public static void main(String[] args) throws IOException {
22 Holder holder = new Holder("config", new NonSerializableResource());
23 Path tempFile = Files.createTempFile("not-serializable", ".ser");
24
25 try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(tempFile))) {
26 out.writeObject(holder);
27 } catch (NotSerializableException e) {
28 System.out.println("Caught: " + e.getClass().getSimpleName());
29 }
30
31 Files.delete(tempFile);
32 }
33}Output:
Caught: NotSerializableException
Assuming a field inherited from a non-serializable superclass survives a serialization round-trip is a second, much subtler mistake — it does not, because deserialization runs that superclass's no-arg constructor fresh rather than restoring its fields from the stream at all.
1// File: NonSerializableSuperclassMistake.java
2import java.io.*;
3import java.nio.file.*;
4
5public class NonSerializableSuperclassMistake {
6
7 static class Base {
8 String label = "default-label";
9 }
10
11 static class Derived extends Base implements Serializable {
12 String name;
13 Derived(String name, String label) {
14 this.name = name;
15 this.label = label;
16 }
17 }
18
19 public static void main(String[] args) throws IOException, ClassNotFoundException {
20 Derived original = new Derived("item-1", "custom-label");
21 Path tempFile = Files.createTempFile("superclass-demo", ".ser");
22
23 try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(tempFile))) {
24 out.writeObject(original);
25 }
26
27 Derived restored;
28 try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(tempFile))) {
29 restored = (Derived) in.readObject();
30 }
31
32 System.out.println("Original label: " + original.label);
33 System.out.println("Restored label: " + restored.label);
34
35 Files.delete(tempFile);
36 }
37}Output:
Original label: custom-label
Restored label: default-label
Base is not Serializable, so its label field is never written to the stream at all — only Derived's own name field is. On the way back, Base's no-arg constructor runs to initialize the Base portion of the restored object, resetting label to its field initializer's value, "default-label", with no connection at all to what original.label actually held.
Interview Questions
Q1. What is the purpose of the Serializable interface, and what methods does it require implementing?
Serializable is a marker interface with no methods at all — implementing it simply flags a class as eligible for serialization, which ObjectOutputStream checks for via instanceof before allowing an object of that type to be written. The nuance interviewers listen for is recognizing "marker interface" as a real design pattern, not just knowing the syntax.
Q2. What is serialVersionUID, and what happens if it's not declared explicitly?
It is a version identifier used during deserialization to confirm the class reading the data matches the class that wrote it — a mismatch throws InvalidClassException. Without an explicit declaration, the JVM computes one automatically from the class's structure, which can change even for a harmless edit like adding a method, silently breaking compatibility with data serialized by an earlier version of the class. Product-company interviewers often ask this specifically to see if you've hit this in a real versioning incident.
Q3. What does the transient keyword do, and what value does a transient field have after deserialization?
It excludes a field from serialization entirely. After deserialization, a transient field holds its type's default value — null for a reference type, as demonstrated in this article's TransientFieldExample — with no connection to whatever value it held when the object was originally serialized. The nuance being tested is that default value, since many candidates assume it's just skipped rather than reset.
Q4. Are constructors called during deserialization?
No, not for the serialized class itself or any of its serializable ancestors — their fields are restored directly from the byte stream via reflection. The one exception is the first non-serializable ancestor in the class hierarchy, whose no-arg constructor does run, exactly as demonstrated in this article's Common Mistakes section. This exception is exactly what separates a strong answer from a memorized one.
Q5. What happens to a field declared in a non-serializable superclass when a serializable subclass is deserialized?
It is not restored from the serialized data at all, since a non-serializable class's fields are never written to the stream in the first place. Instead, it ends up with whatever value the non-serializable superclass's no-arg constructor sets it to when that constructor runs during deserialization. Interviewers listen for whether you can trace through why, not just state the outcome.
Q6. What exception is thrown if an object graph being serialized contains a field of a non-serializable type?
NotSerializableException, thrown the moment the serialization process reaches that field, exactly as demonstrated in this article's NotSerializableMistake example.
Q7. Can serialization be used to create a deep copy of an object?
Yes — serializing an object and immediately deserializing the result produces an entirely independent copy of the whole object graph, since every reachable object is reconstructed fresh. It works reliably but is relatively slow compared to a purpose-built deep-copy method. A strong answer names the performance tradeoff, not just the technique itself.
FAQs
Does implementing Serializable automatically make all of a class's fields persist correctly?
Only for fields whose own types are also serializable — a field of a non-serializable type throws NotSerializableException, and a field inherited from a non-serializable superclass is never persisted at all, as covered in this article's Common Mistakes section.
Can static fields be serialized?
No. Serialization only captures instance state — a static field belongs to the class itself, not to any particular instance, so it is never written to or restored from the serialized stream.
What is the difference between Serializable and Externalizable?
Serializable relies entirely on Java's default, automatic field-by-field mechanism. Externalizable requires the class to implement writeExternal() and readExternal() itself, taking full manual control over exactly what gets written and read — covered in full in this section's dedicated Externalization article.
Is the serialized file format specific to Java, or can other languages read it?
It is Java-specific. The byte stream ObjectOutputStream produces is tied closely to Java's own class and object model, making it unsuitable for exchanging data with a system written in a different language — formats like JSON are the standard choice when cross-language compatibility matters.
Does serialization work with private fields?
Yes. Java's serialization mechanism uses reflection to read and write a class's fields directly, bypassing normal access control entirely — private fields are serialized and restored exactly the same way public ones are.
What happens if a class's fields change between when an object was serialized and when it's deserialized?
If serialVersionUID still matches, Java's default deserialization is reasonably tolerant — a newly added field simply gets its default value, and data for a removed field is ignored. If serialVersionUID does not match, or was never declared explicitly and the computed value has shifted, deserialization fails with InvalidClassException instead.
Is Java serialization considered safe to use with untrusted input?
No. Deserializing data from an untrusted source is a well-documented and serious security risk in Java — a crafted byte stream can trigger unintended code execution through classes already present on the classpath, a category of vulnerability commonly called a deserialization gadget chain. Serialization should only ever be used with data the application itself produced and controls, never with input received from an untrusted client.
Summary
Serialization turns an entire Java object graph into a byte stream and back, replacing the tedious, error-prone work of writing each field out manually with a single writeObject() / readObject() call — as long as every reachable class in that graph is itself serializable. transient opts individual fields out of that process entirely, and an explicit serialVersionUID keeps version compatibility under deliberate control rather than left to a fragile, automatically computed value.
The habit worth carrying forward from this article's shopping cart example is verifying every field's type is genuinely serializable before relying on a class in production, and remembering that a non-serializable superclass's fields are never restored from the stream at all — they come back from that superclass's own no-arg constructor instead, regardless of what they held when the object was originally serialized.