Skip to content
Java io 5 min read

Object Streams

Object streams let you write a complete Java object — all its fields and nested objects — directly to a stream, and read it back later as a fully reconstructed instance. This is called serialization (writing) and deserialization (reading), and it is the backbone of features like saving application state, caching, and passing objects across a network.

The Core Classes

Java provides two classes in java.io for working with object streams:

ClassDirectionWhat it wraps
ObjectOutputStreamWriting (serializing)Any OutputStream (e.g., FileOutputStream)
ObjectInputStreamReading (deserializing)Any InputStream (e.g., FileInputStream)

Both classes implement Closeable, so always use them inside a try-with-resources block.

Making a Class Serializable

Before you can write an object to a stream, its class must implement java.io.Serializable. This is a marker interface — it has no methods; it simply tells the JVM that instances of the class are safe to serialize.

import java.io.Serializable;

public class Student implements Serializable {

    // Recommended: declare a fixed serialVersionUID
    private static final long serialVersionUID = 1L;

    private String name;
    private int    age;

    public Student(String name, int age) {
        this.name = name;
        this.age  = age;
    }

    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + "}";
    }
}

Tip: Always declare serialVersionUID explicitly. If you don’t, the JVM generates one automatically from the class structure. Any future change to the class (adding a field, renaming a method) will change the auto-generated UID, causing an InvalidClassException when you try to read old data.

Writing an Object — ObjectOutputStream

import java.io.*;

public class WriteObjectDemo {
    public static void main(String[] args) throws IOException {

        Student student = new Student("Alice", 22);

        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("student.ser"))) {

            oos.writeObject(student);
            System.out.println("Object written: " + student);
        }
    }
}

Output:

Object written: Student{name='Alice', age=22}

writeObject() serializes the entire object graph — if Student had a Address field that also implements Serializable, that object would be serialized too, recursively.

Reading an Object — ObjectInputStream

import java.io.*;

public class ReadObjectDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("student.ser"))) {

            Student student = (Student) ois.readObject();
            System.out.println("Object read: " + student);
        }
    }
}

Output:

Object read: Student{name='Alice', age=22}

Note: readObject() returns Object, so you must cast to the expected type. The method also declares ClassNotFoundException — thrown if the JVM cannot find the class of the object being deserialized (common when sharing serialized data between different applications or classpaths).

Serializing Multiple Objects

You can write and read multiple objects from the same stream. Just make sure to read them back in the same order they were written.

import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class MultiObjectDemo {
    public static void main(String[] args) throws Exception {

        List<Student> students = List.of(
            new Student("Alice", 22),
            new Student("Bob",   25),
            new Student("Carol", 21)
        );

        // Write
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("students.ser"))) {
            for (Student s : students) {
                oos.writeObject(s);
            }
        }

        // Read
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("students.ser"))) {
            try {
                while (true) {
                    Student s = (Student) ois.readObject();
                    System.out.println(s);
                }
            } catch (EOFException e) {
                System.out.println("All objects read.");
            }
        }
    }
}

Output:

Student{name='Alice', age=22}
Student{name='Bob', age=25}
Student{name='Carol', age=21}
All objects read.

Tip: A cleaner approach for multiple objects is to serialize a List or ArrayList as a single object — one writeObject() and one readObject() call handles the whole collection.

Skipping Fields — the transient Keyword

Sometimes a field should not be serialized: passwords, open file handles, database connections, or fields that can be re-computed at runtime. Mark them transient and the serialization engine skips them. On deserialization those fields will hold their default value (null for objects, 0 for numbers, false for booleans).

import java.io.Serializable;

public class UserAccount implements Serializable {
    private static final long serialVersionUID = 1L;

    private String   username;
    private transient String password; // NOT serialized

    public UserAccount(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public String toString() {
        return "UserAccount{username='" + username + "', password='" + password + "'}";
    }
}

After a round-trip through object streams, password will be null.

See the dedicated transient Keyword page for a full deep-dive.

Under the Hood

The Serialization Stream Format

When ObjectOutputStream writes an object it emits a binary stream with a magic number (0xACED) and version (0x0005), followed by class descriptor blocks (class name, serialVersionUID, field names and types) and then the field values. This format is defined by the Java Object Serialization Specification.

Because the class descriptor is embedded in the stream, the receiver can validate it against the local class definition — that is exactly how serialVersionUID mismatch detection works.

The Object Graph and Shared References

Java’s serialization tracks every object it has already written in the current stream using an internal handle table. If the same object is referenced from two different places in your object graph, it is written only once — subsequent references emit a back-reference handle. This preserves referential integrity on deserialization: both references will point to the same reconstructed instance.

Performance Considerations

Default Java serialization is convenient but not the fastest option. The binary format is verbose (it carries class metadata), and reflection is used to access fields at runtime. For high-throughput or cross-language scenarios, consider:

  • Protocol Buffers / FlatBuffers — compact binary formats
  • Jackson / Gson — JSON-based, human-readable
  • Kryo — a faster Java-specific binary serializer

Security Warning

Warning: Never deserialize data from an untrusted source using standard ObjectInputStream. Carefully crafted byte sequences can exploit the deserialization mechanism to execute arbitrary code (a well-known Java security vulnerability class). Always validate the source and consider using ObjectInputFilter (added in Java 9) to restrict which classes may be deserialized.

You can set a filter like this (Java 9+):

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"));
ois.setObjectInputFilter(info -> {
    if (info.serialClass() == Student.class) return ObjectInputFilter.Status.ALLOWED;
    return ObjectInputFilter.Status.REJECTED;
});

Quick Reference

MethodClassPurpose
writeObject(Object)ObjectOutputStreamSerialize an object
readObject()ObjectInputStreamDeserialize an object
writeInt(int)ObjectOutputStreamWrite a primitive int
readInt()ObjectInputStreamRead a primitive int
defaultWriteObject()ObjectOutputStreamUsed inside custom writeObject()
defaultReadObject()ObjectInputStreamUsed inside custom readObject()

Note: You can customize serialization by declaring private void writeObject(ObjectOutputStream out) and private void readObject(ObjectInputStream in) methods in your class. The JVM will call them automatically instead of the default mechanism.

  • Serialization — the full serialization lifecycle, Externalizable, and versioning strategies
  • transient Keyword — controlling which fields are excluded from serialization
  • FileInputStream — the underlying stream that object streams typically wrap for file I/O
  • FileOutputStream — the write-side counterpart for object stream backing files
  • Byte vs Character Streams — understanding where object streams fit in the Java I/O hierarchy
  • Java I/O — overview of the complete Java I/O framework
Last updated June 13, 2026
Was this helpful?