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:
| Class | Direction | What it wraps |
|---|---|---|
ObjectOutputStream | Writing (serializing) | Any OutputStream (e.g., FileOutputStream) |
ObjectInputStream | Reading (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
serialVersionUIDexplicitly. 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 anInvalidClassExceptionwhen 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()returnsObject, so you must cast to the expected type. The method also declaresClassNotFoundException— 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
ListorArrayListas a single object — onewriteObject()and onereadObject()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 usingObjectInputFilter(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
| Method | Class | Purpose |
|---|---|---|
writeObject(Object) | ObjectOutputStream | Serialize an object |
readObject() | ObjectInputStream | Deserialize an object |
writeInt(int) | ObjectOutputStream | Write a primitive int |
readInt() | ObjectInputStream | Read a primitive int |
defaultWriteObject() | ObjectOutputStream | Used inside custom writeObject() |
defaultReadObject() | ObjectInputStream | Used inside custom readObject() |
Note: You can customize serialization by declaring
private void writeObject(ObjectOutputStream out)andprivate void readObject(ObjectInputStream in)methods in your class. The JVM will call them automatically instead of the default mechanism.
Related Topics
- 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