Contents

A class becomes serializable by implementing Serializable, which is a marker interface with no methods. ObjectOutputStream.writeObject() traverses the entire object graph — the object and all reachable non-transient fields — and encodes them as a byte stream. ObjectInputStream.readObject() reconstructs the graph from those bytes, returning the root object which you cast to the expected type. Every class in the graph must also implement Serializable, or serialization throws a NotSerializableException.

import java.io.*; import java.util.*; // Serializable is a marker interface — no methods to implement class Person implements Serializable { private String name; private int age; private List<String> hobbies; public Person(String name, int age, List<String> hobbies) { this.name = name; this.age = age; this.hobbies = hobbies; } @Override public String toString() { return name + " (" + age + ") " + hobbies; } } // Serialize to a file Person alice = new Person("Alice", 30, List.of("reading", "hiking")); try (ObjectOutputStream oos = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream("person.ser")))) { oos.writeObject(alice); System.out.println("Serialized: " + alice); } // Deserialize from the file try (ObjectInputStream ois = new ObjectInputStream( new BufferedInputStream(new FileInputStream("person.ser")))) { Person restored = (Person) ois.readObject(); System.out.println("Deserialized: " + restored); // Deserialized: Alice (30) [reading, hiking] } // ObjectOutputStream also handles primitive types and graphs // oos.writeInt(42), oos.writeUTF("hello"), oos.writeObject(list) // Serialize to byte array (useful for caching/transmission) ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(alice); } byte[] bytes = baos.toByteArray(); System.out.println("Byte size: " + bytes.length);

The transient keyword marks a field to be excluded from serialization. Use it for passwords, cached derived values, non-serializable objects like database connections, and anything that should not leave the JVM. After deserialization, transient fields are initialised to their default values (null, 0, false). serialVersionUID is a version stamp embedded in the serialized stream; during deserialization the JVM compares it to the local class's UID and throws InvalidClassException if they differ. Always declare it explicitly — without it, the JVM computes one automatically from the class structure, so any change to the class (adding a field, changing a method) silently breaks compatibility with previously serialized data.

class SecureUser implements Serializable { // serialVersionUID — controls version compatibility // If you change the class, increment this to force InvalidClassException on old data // If omitted, JVM computes one — but any class change may break compatibility private static final long serialVersionUID = 1L; private String username; private String email; // transient — field is NOT serialized // Use for: passwords, caches, derived data, non-serializable objects private transient String password; // security — never serialize private transient Connection dbConnection; // not serializable anyway private transient int cachedHashCode; // can be recomputed public SecureUser(String username, String email, String password) { this.username = username; this.email = email; this.password = password; } // After deserialization, transient fields are their default values: // null for objects, 0 for int, false for boolean public String getPassword() { return password; } // will be null after deserialization } // Demonstrating serialVersionUID mismatch // Changing the class after serializing data without updating serialVersionUID // causes java.io.InvalidClassException: local class incompatible: ... // stream classdesc serialVersionUID = 1, local class serialVersionUID = 2 // Best practice: always declare serialVersionUID explicitly // Your IDE can generate it for you Always declare serialVersionUID explicitly. Without it, any change to the class — adding a field, changing a method signature — can break deserialization of existing data with an InvalidClassException.

Declaring private void writeObject(ObjectOutputStream) and private void readObject(ObjectInputStream) in a Serializable class hooks into the serialization process without replacing it entirely. Call defaultWriteObject() or defaultReadObject() first to handle all non-transient fields automatically, then add custom logic: writing extra version tags, initialising transient fields from the deserialized data, or performing cross-field validation after reading. This is the primary mechanism for schema migration when a class evolves between serialized versions.

class Product implements Serializable { private static final long serialVersionUID = 1L; private String name; private double price; private transient String cachedDisplay; // derived, not serialized public Product(String name, double price) { this.name = name; this.price = price; this.cachedDisplay = name + " $" + price; } // Custom serialization — called by ObjectOutputStream private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); // serialize all non-transient fields // Write additional data if needed oos.writeUTF("extra-metadata"); } // Custom deserialization — called by ObjectInputStream private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); // deserialize all non-transient fields String meta = ois.readUTF(); // read extra data // Rebuild transient fields this.cachedDisplay = name + " $" + price; } // readObjectNoData — called when class has no data in stream (version mismatch) private void readObjectNoData() throws ObjectStreamException { this.name = "Unknown"; this.price = 0.0; } } // readResolve — control what object is actually returned during deserialization // Useful for Singletons and enums class Config implements Serializable { private static final long serialVersionUID = 1L; private static final Config INSTANCE = new Config(); private Config() {} public static Config getInstance() { return INSTANCE; } // Ensure singleton property after deserialization private Object readResolve() { return INSTANCE; // discard deserialized object, return the real singleton } }

Externalizable extends Serializable but requires you to implement writeExternal() and readExternal() explicitly. Nothing is serialized automatically — you write and read every field yourself, giving complete control over the wire format and the ability to omit fields, apply custom encoding, or achieve a more compact representation. The tradeoff is more boilerplate. One hard requirement: the class must have a public no-arg constructor, which is called during deserialization before readExternal() populates the fields.

import java.io.*; // Externalizable — complete manual control; no automatic serialization // Must implement writeExternal and readExternal // Must have a public no-arg constructor (called during deserialization) class Point3D implements Externalizable { private double x, y, z; public Point3D() {} // REQUIRED for Externalizable public Point3D(double x, double y, double z) { this.x = x; this.y = y; this.z = z; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeDouble(x); out.writeDouble(y); out.writeDouble(z); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { x = in.readDouble(); y = in.readDouble(); z = in.readDouble(); } @Override public String toString() { return "(" + x + "," + y + "," + z + ")"; } } // Usage is the same as Serializable — use ObjectOutputStream/InputStream Point3D p = new Point3D(1.0, 2.0, 3.0); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(p); } try (ObjectInputStream ois = new ObjectInputStream( new ByteArrayInputStream(baos.toByteArray()))) { Point3D restored = (Point3D) ois.readObject(); System.out.println(restored); // (1.0,2.0,3.0) } // Externalizable is more efficient but requires more code // Serializable is simpler; Externalizable gives full control over format

Deserializing data from untrusted sources is a well-known attack vector: carefully crafted byte streams can trigger arbitrary code execution during deserialization through chains of gadget classes already on the classpath. For data exchange with external systems, prefer formats like JSON or Protocol Buffers where deserialization does not execute application code. If Java serialization is unavoidable, use ObjectInputFilter (Java 9+) to whitelist the specific classes that are expected in the stream and reject everything else. Never deserialize input from the network, user-uploaded files, or any source you do not fully control without such a filter in place.

// SECURITY WARNING: Deserializing untrusted data is dangerous // Attackers can craft byte streams that execute arbitrary code during deserialization // This is one of the most critical Java security vulnerabilities // Java 9+: ObjectInputFilter — allowlist/denylist deserialization ObjectInputStream ois = new ObjectInputStream(inputStream); // Set a filter that only allows Person class and standard types ObjectInputFilter filter = ObjectInputFilter.Config.createFilter( "com.example.Person;java.util.*;!*" // allow Person and java.util, deny rest ); ois.setObjectInputFilter(filter); // Global filter (Java 9+ system property or programmatic) ObjectInputFilter.Config.setSerialFilter(filter); // Best practices: // 1. Never deserialize data from untrusted sources (network, files, user input) // 2. Use allowlist filters for any deserialization // 3. Consider alternatives to Java serialization: // - JSON (Jackson, Gson) — human-readable, widely supported // - Protocol Buffers — compact, schema-based, language-agnostic // - MessagePack — compact binary, no code generation needed // - Kryo — fast Java serialization library // Enum serialization — enums are serialized by name, not structure // Always safe — readResolve() is automatically used to return the enum constant enum Status { ACTIVE, INACTIVE } // Serializing Status.ACTIVE works correctly across class versions // Records (Java 16+) are serializable if they implement Serializable // Serialized form uses the canonical constructor — safer than class serialization Never deserialize data from untrusted sources using Java's built-in serialization. Use ObjectInputFilter as a mitigation, but strongly prefer JSON or Protocol Buffers for data exchange with external systems.